feat: 添加CreateFeed功能及相关视图组件

- 新增CreateFeedView和CreateFeedFeature,支持用户发布图文动态。
- 在FeedView中集成CreateFeedView,允许用户通过加号按钮访问发布界面。
- 实现图片选择和文本输入功能,支持最多9张图片的上传。
- 添加发布API请求模型,处理动态发布逻辑。
- 更新FeedFeature以管理CreateFeedView的显示状态,确保用户体验流畅。
- 完善UI结构分析与执行计划文档,明确开发步骤和技术要点。
This commit is contained in:
edwinQQQ
2025-07-16 15:53:32 +08:00
parent 33a558ae7b
commit 4bbb4f8434
5 changed files with 703 additions and 2 deletions

View File

@@ -0,0 +1,223 @@
import Foundation
import ComposableArchitecture
import SwiftUI
// PhotosUI (iOS 16.0+)
#if canImport(PhotosUI)
import PhotosUI
#endif
@Reducer
struct CreateFeedFeature {
@ObservableState
struct State: Equatable {
var content: String = ""
var processedImages: [UIImage] = []
var isLoading: Bool = false
var errorMessage: String? = nil
var characterCount: Int = 0
// iOS 16+ PhotosPicker
#if canImport(PhotosUI) && swift(>=5.7)
var selectedImages: [PhotosPickerItem] = []
#endif
// iOS 15 UIImagePickerController
var showingImagePicker: Bool = false
var canAddMoreImages: Bool {
processedImages.count < 9
}
var canPublish: Bool {
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading
}
}
enum Action {
case contentChanged(String)
case publishButtonTapped
case publishResponse(Result<PublishDynamicResponse, Error>)
case clearError
case dismissView
// iOS 16+ PhotosPicker Actions
#if canImport(PhotosUI) && swift(>=5.7)
case photosPickerItemsChanged([PhotosPickerItem])
case processPhotosPickerItems([PhotosPickerItem])
#endif
// iOS 15 UIImagePickerController Actions
case showImagePicker
case hideImagePicker
case imageSelected(UIImage)
case removeImage(Int)
}
@Dependency(\.apiService) var apiService
@Dependency(\.dismiss) var dismiss
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .contentChanged(let newContent):
state.content = newContent
state.characterCount = newContent.count
return .none
#if canImport(PhotosUI) && swift(>=5.7)
case .photosPickerItemsChanged(let items):
state.selectedImages = items
return .run { send in
await send(.processPhotosPickerItems(items))
}
case .processPhotosPickerItems(let items):
return .run { [currentImages = state.processedImages] send in
var newImages = currentImages
for item in items {
if let data = try? await item.loadTransferable(type: Data.self),
let image = UIImage(data: data) {
if newImages.count < 9 {
newImages.append(image)
}
}
}
await MainActor.run {
state.processedImages = newImages
}
}
#endif
case .showImagePicker:
state.showingImagePicker = true
return .none
case .hideImagePicker:
state.showingImagePicker = false
return .none
case .imageSelected(let image):
if state.processedImages.count < 9 {
state.processedImages.append(image)
}
state.showingImagePicker = false
return .none
case .removeImage(let index):
guard index < state.processedImages.count else { return .none }
state.processedImages.remove(at: index)
#if canImport(PhotosUI) && swift(>=5.7)
if index < state.selectedImages.count {
state.selectedImages.remove(at: index)
}
#endif
return .none
case .publishButtonTapped:
guard state.canPublish else {
state.errorMessage = "请输入内容"
return .none
}
state.isLoading = true
state.errorMessage = nil
let request = PublishDynamicRequest(
content: state.content.trimmingCharacters(in: .whitespacesAndNewlines),
images: state.processedImages
)
return .run { send in
do {
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 .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 dismiss()
}
}
}
}
}
// MARK: -
///
struct PublishDynamicRequest: APIRequestProtocol {
typealias Response = PublishDynamicResponse
let endpoint: String = "/dynamic/square/publish" //
let method: HTTPMethod = .POST
let includeBaseParameters: Bool = true
let queryParameters: [String: String]? = nil
let timeout: TimeInterval = 30.0
let content: String
let images: [UIImage]
let type: Int // 0: , 2:
init(content: String, images: [UIImage] = []) {
self.content = content
self.images = images
self.type = images.isEmpty ? 0 : 2
}
var bodyParameters: [String: Any]? {
var params: [String: Any] = [
"content": content,
"type": type
]
// base64
if !images.isEmpty {
let imageData = images.compactMap { image in
image.jpegData(compressionQuality: 0.8)?.base64EncodedString()
}
params["images"] = imageData
}
return params
}
}
///
struct PublishDynamicResponse: Codable {
let code: Int
let message: String
let data: PublishDynamicData?
}
struct PublishDynamicData: Codable {
let dynamicId: Int
let publishTime: Int
}