Files
e-party-iOS/yana/Features/CreateFeedFeature.swift
edwinQQQ 57a8b833eb feat: 更新CreateFeedFeature和FeedListFeature以增强发布和关闭功能
- 修改CreateFeedFeature中的发布逻辑,确保在发布成功时同时发送关闭通知。
- 更新FeedListFeature以在创建Feed成功时触发刷新并关闭编辑页面。
- 优化CreateFeedView中的键盘管理和通知处理,提升用户体验。
2025-07-31 16:44:49 +08:00

300 lines
12 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
@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
//