
- 修改CreateFeedFeature中的发布逻辑,确保在发布成功时同时发送关闭通知。 - 更新FeedListFeature以在创建Feed成功时触发刷新并关闭编辑页面。 - 优化CreateFeedView中的键盘管理和通知处理,提升用户体验。
300 lines
12 KiB
Swift
300 lines
12 KiB
Swift
import Foundation
|
||
import ComposableArchitecture
|
||
import SwiftUI
|
||
import PhotosUI
|
||
|
||
@Reducer
|
||
struct CreateFeedFeature {
|
||
@ObservableState
|
||
struct State: Equatable {
|
||
var content: String = ""
|
||
var processedImages: [UIImage] = []
|
||
var errorMessage: String? = nil
|
||
var characterCount: Int = 0
|
||
var selectedImages: [PhotosPickerItem] = []
|
||
var canAddMoreImages: Bool {
|
||
processedImages.count < 9
|
||
}
|
||
var canPublish: Bool {
|
||
(!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || !processedImages.isEmpty) && !isLoading
|
||
}
|
||
var isLoading: Bool = false
|
||
|
||
// 新增:图片上传相关状态
|
||
var uploadedImageUrls: [String] = []
|
||
var uploadedImages: [UIImage] = [] // 保存原始图片用于获取尺寸信息
|
||
var isUploadingImages: Bool = false
|
||
var uploadProgress: Double = 0.0
|
||
var uploadStatus: String = ""
|
||
|
||
init() {
|
||
// 默认初始化
|
||
}
|
||
}
|
||
|
||
enum Action {
|
||
case contentChanged(String)
|
||
case publishButtonTapped
|
||
case publishResponse(Result<PublishFeedResponse, Error>)
|
||
case clearError
|
||
case dismissView
|
||
case photosPickerItemsChanged([PhotosPickerItem])
|
||
case processPhotosPickerItems([PhotosPickerItem])
|
||
case removeImage(Int)
|
||
case updateProcessedImages([UIImage])
|
||
|
||
// 新增:图片上传相关 Action
|
||
case uploadImagesToCOS
|
||
case imageUploadProgress(Double, Int, Int) // progress, current, total
|
||
case imageUploadCompleted([String], [UIImage]) // urls, images
|
||
case imageUploadFailed(Error)
|
||
case publishContent
|
||
|
||
// 新增:发布成功通知
|
||
case publishSuccess
|
||
}
|
||
|
||
@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
|
||
state.characterCount = newContent.count
|
||
return .none
|
||
|
||
case .photosPickerItemsChanged(let items):
|
||
state.selectedImages = 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 send(.updateProcessedImages(newImages))
|
||
}
|
||
|
||
case .updateProcessedImages(let images):
|
||
state.processedImages = images
|
||
// 清空之前的上传结果
|
||
state.uploadedImageUrls = []
|
||
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.uploadedImageUrls.count {
|
||
state.uploadedImageUrls.remove(at: index)
|
||
}
|
||
return .none
|
||
|
||
case .publishButtonTapped:
|
||
guard state.canPublish else {
|
||
state.errorMessage = "请输入内容或选择图片"
|
||
return .none
|
||
}
|
||
|
||
// 如果有图片且还没有上传,先上传图片
|
||
if !state.processedImages.isEmpty && state.uploadedImageUrls.isEmpty {
|
||
return .send(.uploadImagesToCOS)
|
||
}
|
||
|
||
// 直接发布(图片已上传或没有图片)
|
||
return .send(.publishContent)
|
||
|
||
case .uploadImagesToCOS:
|
||
guard !state.processedImages.isEmpty else {
|
||
return .send(.publishContent)
|
||
}
|
||
|
||
state.isUploadingImages = true
|
||
state.uploadProgress = 0.0
|
||
state.uploadStatus = "正在上传图片..."
|
||
state.errorMessage = nil
|
||
|
||
// 提取状态值到局部变量,避免在 @Sendable 闭包中访问 inout 参数
|
||
let imagesToUpload = state.processedImages
|
||
|
||
return .run { send in
|
||
var uploadedUrls: [String] = []
|
||
var uploadedImages: [UIImage] = []
|
||
let totalImages = imagesToUpload.count
|
||
|
||
for (index, image) in imagesToUpload.enumerated() {
|
||
// 更新上传进度
|
||
await send(.imageUploadProgress(Double(index) / Double(totalImages), index + 1, totalImages))
|
||
|
||
// 上传图片到 COS
|
||
if let imageUrl = await COSManager.shared.uploadUIImage(image, apiService: apiService) {
|
||
uploadedUrls.append(imageUrl)
|
||
uploadedImages.append(image) // 保存原始图片
|
||
} else {
|
||
// 上传失败
|
||
await send(.imageUploadFailed(APIError.custom("图片上传失败")))
|
||
return
|
||
}
|
||
}
|
||
|
||
// 所有图片上传完成
|
||
await send(.imageUploadProgress(1.0, totalImages, totalImages))
|
||
await send(.imageUploadCompleted(uploadedUrls, uploadedImages))
|
||
}
|
||
|
||
case .imageUploadProgress(let progress, let current, let total):
|
||
state.uploadProgress = progress
|
||
state.uploadStatus = "正在上传图片... (\(current)/\(total))"
|
||
return .none
|
||
|
||
case .imageUploadCompleted(let urls, let images):
|
||
state.isUploadingImages = false
|
||
state.uploadedImageUrls = urls
|
||
state.uploadedImages = images
|
||
state.uploadStatus = "图片上传完成"
|
||
// 上传完成后自动发布内容
|
||
return .send(.publishContent)
|
||
|
||
case .imageUploadFailed(let error):
|
||
state.isUploadingImages = false
|
||
state.errorMessage = "图片上传失败: \(error.localizedDescription)"
|
||
return .none
|
||
|
||
case .publishContent:
|
||
state.isLoading = true
|
||
state.errorMessage = nil
|
||
|
||
// 提取状态值到局部变量,避免在 @Sendable 闭包中访问 inout 参数
|
||
let content = state.content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
let imageUrls = state.uploadedImageUrls
|
||
let images = state.uploadedImages
|
||
|
||
return .run { send in
|
||
do {
|
||
// 构建 ResListItem 数组
|
||
var resList: [ResListItem] = []
|
||
for (index, imageUrl) in imageUrls.enumerated() {
|
||
if index < images.count, let cgImage = images[index].cgImage {
|
||
let width = cgImage.width
|
||
let height = cgImage.height
|
||
let format = "jpeg"
|
||
let item = ResListItem(resUrl: imageUrl, width: width, height: height, format: format)
|
||
resList.append(item)
|
||
}
|
||
}
|
||
|
||
// 使用 PublishFeedRequest 而不是 PublishDynamicRequest
|
||
let userId = await UserInfoManager.getCurrentUserId() ?? ""
|
||
let type = resList.isEmpty ? "0" : "2" // 0: 纯文字, 2: 图片
|
||
let request = await PublishFeedRequest.make(
|
||
content: content.isEmpty ? "" : content,
|
||
uid: userId,
|
||
type: type,
|
||
resList: resList.isEmpty ? nil : resList
|
||
)
|
||
|
||
let response = try await apiService.request(request)
|
||
await send(.publishResponse(.success(response)))
|
||
} catch {
|
||
await send(.publishResponse(.failure(error)))
|
||
}
|
||
}
|
||
|
||
case .publishResponse(.success(let response)):
|
||
state.isLoading = false
|
||
if response.code == 200 {
|
||
// 发布成功,先发送通知,然后关闭页面
|
||
return .merge(
|
||
.send(.publishSuccess),
|
||
.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:
|
||
// 始终发送通知,让外层处理关闭逻辑
|
||
return .run { _ in
|
||
await MainActor.run {
|
||
NotificationCenter.default.post(name: .init("CreateFeedDismiss"), object: nil)
|
||
}
|
||
}
|
||
case .publishSuccess:
|
||
// 发送通知给外层刷新列表和关闭页面
|
||
return .merge(
|
||
.run { _ in
|
||
await MainActor.run {
|
||
NotificationCenter.default.post(name: .init("CreateFeedPublishSuccess"), object: nil)
|
||
}
|
||
},
|
||
.run { _ in
|
||
await MainActor.run {
|
||
NotificationCenter.default.post(name: .init("CreateFeedDismiss"), object: nil)
|
||
}
|
||
}
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
extension CreateFeedFeature.Action: Equatable {
|
||
static func == (lhs: CreateFeedFeature.Action, rhs: CreateFeedFeature.Action) -> Bool {
|
||
switch (lhs, rhs) {
|
||
case let (.contentChanged(a), .contentChanged(b)):
|
||
return a == b
|
||
case (.publishButtonTapped, .publishButtonTapped):
|
||
return true
|
||
case (.clearError, .clearError):
|
||
return true
|
||
case (.dismissView, .dismissView):
|
||
return true
|
||
case let (.removeImage(a), .removeImage(b)):
|
||
return a == b
|
||
case (.uploadImagesToCOS, .uploadImagesToCOS):
|
||
return true
|
||
case let (.imageUploadProgress(a, b, c), .imageUploadProgress(d, e, f)):
|
||
return a == d && b == e && c == f
|
||
case let (.imageUploadCompleted(a, c), .imageUploadCompleted(b, d)):
|
||
return a == b && c.count == d.count // 简化比较,只比较URL数组和图片数量
|
||
case let (.imageUploadFailed(a), .imageUploadFailed(b)):
|
||
return a.localizedDescription == b.localizedDescription
|
||
case (.publishContent, .publishContent):
|
||
return true
|
||
case (.publishSuccess, .publishSuccess):
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 发布动态相关模型
|
||
|
||
// 注意:现在使用 DynamicsModels.swift 中的 PublishFeedRequest 和 PublishFeedResponse
|
||
// 不再需要重复定义这些模型
|