Files
e-party-iOS/yana/Features/EditFeedFeature.swift
edwinQQQ beda539e00 feat: 添加COSManagerAdapter以支持新的TCCos组件
- 新增COSManagerAdapter类,保持与现有COSManager相同的接口,内部使用新的TCCos组件。
- 实现获取、刷新Token及上传图片的功能,确保与腾讯云COS的兼容性。
- 在CreateFeedView中重构内容输入、图片选择和发布按钮逻辑,提升用户体验。
- 更新EditFeedView以优化视图结构和状态管理,确保功能正常运行。
- 在多个视图中添加键盘状态管理,改善用户交互体验。
2025-07-31 11:41:38 +08:00

259 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import ComposableArchitecture
import SwiftUI
import PhotosUI // PhotosUI
@Reducer
struct EditFeedFeature {
@ObservableState
struct State: Equatable {
var content: String = ""
var isLoading: Bool = false
var errorMessage: String? = nil
var shouldDismiss: Bool = false
var selectedImages: [PhotosPickerItem] = []
var processedImages: [UIImage] = []
var canAddMoreImages: Bool {
processedImages.count < 9
}
var canPublish: Bool {
(!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || !processedImages.isEmpty) && !isLoading && !isUploadingImages
}
//
var isUploadingImages: Bool = false
var imageUploadProgress: Double = 0.0 // 0.0~1.0
var uploadedResList: [ResListItem] = []
// PhotosPicker
var showPhotosPicker: Bool = false
var selectedPhotoItems: [PhotosPickerItem] = []
//
var showDeleteImageAlert: Bool = false
var imageToDeleteIndex: Int? = nil
//
init() {
//
}
// EquatableselectedImagesPhotosPickerItemEquatable
static func == (lhs: State, rhs: State) -> Bool {
lhs.content == rhs.content &&
lhs.isLoading == rhs.isLoading &&
lhs.errorMessage == rhs.errorMessage &&
lhs.shouldDismiss == rhs.shouldDismiss &&
lhs.processedImages == rhs.processedImages &&
lhs.selectedImages.count == rhs.selectedImages.count &&
lhs.isUploadingImages == rhs.isUploadingImages &&
lhs.imageUploadProgress == rhs.imageUploadProgress &&
lhs.uploadedResList == rhs.uploadedResList &&
lhs.showPhotosPicker == rhs.showPhotosPicker &&
lhs.selectedPhotoItems.count == rhs.selectedPhotoItems.count &&
lhs.showDeleteImageAlert == rhs.showDeleteImageAlert &&
lhs.imageToDeleteIndex == rhs.imageToDeleteIndex
}
}
enum Action {
case contentChanged(String)
case publishButtonTapped
case publishResponse(Result<PublishFeedResponse, Error>)
case clearError
case dismissView
case clearDismissFlag
// Action
case photosPickerItemsChanged([PhotosPickerItem])
case processPhotosPickerItems([PhotosPickerItem])
case updateProcessedImages([UIImage])
case removeImage(Int)
// Action
case uploadImages
case uploadImagesResponse(Result<[ResListItem], Error>)
//
case updateImageUploadProgress(Double)
// PhotosPickerAction
case photosPickerDismissed
case addImageButtonTapped
// Action
case showDeleteImageAlert(Int)
case deleteImageAlertDismissed
}
@Dependency(\.apiService) var apiService
@Dependency(\.dismiss) var dismiss
@Dependency(\.isPresented) var isPresented
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .contentChanged(let newContent):
state.content = newContent
return .none
case .publishButtonTapped:
guard state.canPublish else {
state.errorMessage = "请输入内容"
return .none
}
//
if !state.processedImages.isEmpty {
state.isUploadingImages = true
state.imageUploadProgress = 0.0
state.errorMessage = nil
return .send(.uploadImages)
} else {
state.isLoading = true
state.errorMessage = nil
return .run { [content = state.content] send in
let userId = await UserInfoManager.getCurrentUserId() ?? ""
let type = content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "2" : "0"
let request = await PublishFeedRequest.make(
content: content.trimmingCharacters(in: .whitespacesAndNewlines),
uid: userId,
type: type,
resList: nil
)
do {
let response = try await apiService.request(request)
await send(.publishResponse(.success(response)))
} catch {
await send(.publishResponse(.failure(error)))
}
}
}
case .uploadImages:
let images = state.processedImages
return .run { send in
var resList: [ResListItem] = []
for (idx, image) in images.enumerated() {
guard let data = image.jpegData(compressionQuality: 0.9) else { continue }
if let url = await COSManager.shared.uploadImage(data, apiService: apiService),
let cgImage = image.cgImage {
let width = cgImage.width
let height = cgImage.height
let format = "jpeg"
let item = ResListItem(resUrl: url, width: width, height: height, format: format)
resList.append(item)
}
//
await MainActor.run {
send(.updateImageUploadProgress(Double(idx + 1) / Double(images.count)))
}
}
if resList.count == images.count {
await send(.uploadImagesResponse(.success(resList)))
} else {
await send(.uploadImagesResponse(.failure(NSError(domain: "COSUpload", code: -1, userInfo: [NSLocalizedDescriptionKey: "部分图片上传失败"])) ))
}
}
case .uploadImagesResponse(let result):
state.isUploadingImages = false
state.imageUploadProgress = 1.0
switch result {
case .success(let resList):
state.uploadedResList = resList
state.isLoading = true
return .run { [content = state.content, resList] send in
let userId = await UserInfoManager.getCurrentUserId() ?? ""
// type: 2 /
let type = resList.isEmpty ? "0" : (content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "2" : "2")
let request = await PublishFeedRequest.make(
content: content.trimmingCharacters(in: .whitespacesAndNewlines),
uid: userId,
type: type,
resList: resList
)
do {
let response = try await apiService.request(request)
await send(.publishResponse(.success(response)))
} catch {
await send(.publishResponse(.failure(error)))
}
}
case .failure(let error):
state.errorMessage = error.localizedDescription
return .none
}
case .publishResponse(.success(let response)):
state.isLoading = false
if response.code == 200 {
return .send(.dismissView)
} else {
state.errorMessage = response.message.isEmpty ? "发布失败" : response.message
return .none
}
case .publishResponse(.failure(let error)):
state.isLoading = false
state.errorMessage = error.localizedDescription
return .none
case .clearError:
state.errorMessage = nil
return .none
case .dismissView:
state.shouldDismiss = true
return .none
case .clearDismissFlag:
state.shouldDismiss = false
return .none
case .photosPickerItemsChanged(let items):
state.selectedImages = items
state.selectedPhotoItems = items
return .run { send in
await send(.processPhotosPickerItems(items))
}
case .processPhotosPickerItems(let items):
let currentImages = state.processedImages
return .run { send in
var newImages = currentImages
for item in items {
guard let data = try? await item.loadTransferable(type: Data.self),
let image = UIImage(data: data) else { continue }
if newImages.count < 9 {
newImages.append(image)
}
}
await MainActor.run {
send(.updateProcessedImages(newImages))
}
}
case .updateProcessedImages(let images):
state.processedImages = images
return .none
case .removeImage(let index):
guard index < state.processedImages.count else { return .none }
state.processedImages.remove(at: index)
if index < state.selectedImages.count {
state.selectedImages.remove(at: index)
}
if index < state.selectedPhotoItems.count {
state.selectedPhotoItems.remove(at: index)
}
return .none
//
case .updateImageUploadProgress(let progress):
state.imageUploadProgress = progress
return .none
// PhotosPickerAction
case .photosPickerDismissed:
state.showPhotosPicker = false
return .none
case .addImageButtonTapped:
state.showPhotosPicker = true
return .none
// Action
case .showDeleteImageAlert(let index):
state.imageToDeleteIndex = index
state.showDeleteImageAlert = true
return .none
case .deleteImageAlertDismissed:
state.showDeleteImageAlert = false
state.imageToDeleteIndex = nil
return .none
}
}
}
}