feat: 添加CreateFeed功能及相关视图组件
- 新增CreateFeedView和CreateFeedFeature,支持用户发布图文动态。 - 在FeedView中集成CreateFeedView,允许用户通过加号按钮访问发布界面。 - 实现图片选择和文本输入功能,支持最多9张图片的上传。 - 添加发布API请求模型,处理动态发布逻辑。 - 更新FeedFeature以管理CreateFeedView的显示状态,确保用户体验流畅。 - 完善UI结构分析与执行计划文档,明确开发步骤和技术要点。
This commit is contained in:
223
yana/Features/CreateFeedFeature.swift
Normal file
223
yana/Features/CreateFeedFeature.swift
Normal 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
|
||||
}
|
Reference in New Issue
Block a user