Files
e-party-iOS/yana/Features/CreateFeedFeature.swift
edwinQQQ 1f17960b8d feat: 新增CreateFeedView优化任务总结文档及相关功能实现
- 在CreateFeedView中优化发布按钮样式,增加圆角背景和渐变色。
- 移除内容输入区域的深灰色背景,提升UI体验。
- 实现点击发布按钮时自动收起键盘功能。
- 添加发布成功通知机制,确保外层刷新列表数据。
- 更新相关Feature以支持跨Feature通信和状态管理。
2025-07-31 14:23:15 +08:00

293 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 && !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 MainActor.run {
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,
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:
guard isPresented else {
return .none
}
return .run { _ in
await dismiss()
}
case .publishSuccess:
//
return .run { _ in
NotificationCenter.default.post(name: .init("CreateFeedPublishSuccess"), 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
//