Files
e-party-iOS/yana/Features/CreateFeedFeature.swift
edwinQQQ 65c74db837 feat: 更新CreateFeedFeature和CreateFeedView以增强发布功能
- 修改发布逻辑,允许在内容为空时仍可发布图片,提升用户灵活性。
- 更新错误提示信息,明确用户需要输入内容或选择图片。
- 调整发布按钮显示逻辑,仅在键盘隐藏时显示,优化界面布局。
- 增加工具栏标题,提升用户界面友好性。
- 优化发布按钮样式,增加圆角和渐变背景,提升视觉效果。
2025-07-31 16:21:32 +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 || !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 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.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:
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
//