feat: 更新COSManager和相关视图以增强图片上传功能

- 修改COSManagerAdapter以支持新的TCCos组件,确保与腾讯云COS的兼容性。
- 在CreateFeedFeature中新增图片上传相关状态和Action,优化图片选择与上传逻辑。
- 更新CreateFeedView以整合图片上传功能,提升用户体验。
- 在多个视图中添加键盘状态管理,改善用户交互体验。
- 新增COS相关的测试文件,确保功能的正确性和稳定性。
This commit is contained in:
edwinQQQ
2025-07-31 11:41:56 +08:00
parent beda539e00
commit b966e24532
26 changed files with 4641 additions and 371 deletions

View File

@@ -0,0 +1,615 @@
import Foundation
import ComposableArchitecture
import UIKit
// MARK: - COS Feature
/// COS Feature
/// Token
public struct COSFeature: Reducer, @unchecked Sendable {
// MARK: - State
/// COS
public struct State: Equatable {
/// Token
public var tokenState: TokenState?
///
public var uploadState: UploadState?
///
public var configurationState: ConfigurationState?
public init(
tokenState: TokenState? = TokenState(),
uploadState: UploadState? = UploadState(),
configurationState: ConfigurationState? = ConfigurationState()
) {
self.tokenState = tokenState
self.uploadState = uploadState
self.configurationState = configurationState
}
}
// MARK: - Action
/// COS Action
@CasePathable
public enum Action: Equatable {
/// Token Action
case token(TokenAction)
/// Action
case upload(UploadAction)
/// Action
case configuration(ConfigurationAction)
///
case onAppear
///
case handleError(COSError)
///
case retry
///
case resetAll
///
case checkHealth
}
// MARK: - Dependencies
@Dependency(\.cosTokenService) var tokenService
@Dependency(\.cosUploadService) var uploadService
@Dependency(\.cosConfigurationService) var configurationService
// MARK: - Reducer
public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .onAppear:
debugInfoSync("🚀 COS Feature 初始化")
return handleOnAppear()
case .token(let tokenAction):
return handleTokenAction(&state, tokenAction)
case .upload(let uploadAction):
return handleUploadAction(&state, uploadAction)
case .configuration(let configAction):
return handleConfigurationAction(&state, configAction)
case .handleError(let error):
debugErrorSync("❌ COS Feature 错误: \(error.localizedDescription)")
return .none
case .retry:
return handleRetry()
case .resetAll:
return handleResetAll()
case .checkHealth:
return handleCheckHealth()
}
}
.ifLet(\.tokenState, action: /Action.token) {
TokenReducer()
}
.ifLet(\.uploadState, action: /Action.upload) {
UploadReducer()
}
.ifLet(\.configurationState, action: /Action.configuration) {
ConfigurationReducer()
}
}
// MARK: -
/// onAppear
private func handleOnAppear() -> Effect<Action> {
return .run { send in
//
let isInitialized = await configurationService.isCOSServiceInitialized()
await send(.configuration(.initializationStatusReceived(isInitialized)))
// Token
if !isInitialized {
do {
let token = try await tokenService.refreshToken()
await send(.token(.tokenReceived(token)))
await send(.configuration(.initializeService(token)))
} catch {
await send(.handleError(error as? COSError ?? .unknown(error.localizedDescription)))
}
} else {
// Token
let status = await tokenService.getTokenStatus()
await send(.token(.tokenStatusReceived(status)))
}
}
}
///
private func handleRetry() -> Effect<Action> {
return .run { send in
debugInfoSync("🔄 开始重试操作...")
// Token
do {
let token = try await tokenService.refreshToken()
await send(.token(.tokenReceived(token)))
await send(.configuration(.initializeService(token)))
} catch {
await send(.handleError(error as? COSError ?? .unknown(error.localizedDescription)))
}
}
}
///
private func handleResetAll() -> Effect<Action> {
return .run { send in
debugInfoSync("🔄 重置所有状态...")
tokenService.clearCachedToken()
await configurationService.resetCOSService()
await send(.token(.clearToken))
await send(.upload(.reset))
await send(.configuration(.resetService))
}
}
///
private func handleCheckHealth() -> Effect<Action> {
return .run { send in
debugInfoSync("🏥 检查服务健康状态...")
let isInitialized = await configurationService.isCOSServiceInitialized()
let tokenStatus = await tokenService.getTokenStatus()
if !isInitialized {
await send(.handleError(.serviceNotInitialized))
} else if tokenStatus.contains("过期") {
await send(.handleError(.tokenExpired))
} else {
debugInfoSync("✅ 服务健康状态良好")
}
}
}
/// Token Action
private func handleTokenAction(_ state: inout State, _ action: TokenAction) -> Effect<Action> {
switch action {
case .getToken:
return .run { send in
do {
let token = try await tokenService.refreshToken()
await send(.token(.tokenReceived(token)))
} catch {
await send(.token(.setError(error.localizedDescription)))
await send(.handleError(error as? COSError ?? .unknown(error.localizedDescription)))
}
}
case .refreshToken:
return .run { send in
do {
let token = try await tokenService.refreshToken()
await send(.token(.tokenReceived(token)))
// Token
await send(.configuration(.initializeService(token)))
} catch {
await send(.token(.setError(error.localizedDescription)))
await send(.handleError(error as? COSError ?? .unknown(error.localizedDescription)))
}
}
case .getTokenStatus:
return .run { send in
let status = await tokenService.getTokenStatus()
await send(.token(.tokenStatusReceived(status)))
}
case .clearToken:
return .run { send in
tokenService.clearCachedToken()
await send(.configuration(.resetService))
}
case .tokenReceived, .tokenStatusReceived, .setError:
// Action Reducer
return .none
}
}
/// Action
private func handleUploadAction(_ state: inout State, _ action: UploadAction) -> Effect<Action> {
switch action {
case .uploadImage(let imageData, let fileName):
return .run { send in
// Token
let isInitialized = await configurationService.isCOSServiceInitialized()
guard isInitialized else {
await send(.upload(.uploadFailed("服务未初始化")))
await send(.handleError(.serviceNotInitialized))
return
}
let tokenStatus = await tokenService.getTokenStatus()
guard !tokenStatus.contains("过期") else {
await send(.upload(.uploadFailed("Token 已过期")))
await send(.handleError(.tokenExpired))
return
}
do {
let url = try await uploadService.uploadImage(imageData, fileName: fileName)
await send(.upload(.uploadCompleted(url)))
} catch {
await send(.upload(.uploadFailed(error.localizedDescription)))
await send(.handleError(error as? COSError ?? .unknown(error.localizedDescription)))
}
}
case .uploadUIImage(let image, let fileName):
return .run { send in
// Token
let isInitialized = await configurationService.isCOSServiceInitialized()
guard isInitialized else {
await send(.upload(.uploadFailed("服务未初始化")))
await send(.handleError(.serviceNotInitialized))
return
}
let tokenStatus = await tokenService.getTokenStatus()
guard !tokenStatus.contains("过期") else {
await send(.upload(.uploadFailed("Token 已过期")))
await send(.handleError(.tokenExpired))
return
}
do {
let url = try await uploadService.uploadUIImage(image, fileName: fileName)
await send(.upload(.uploadCompleted(url)))
} catch {
await send(.upload(.uploadFailed(error.localizedDescription)))
await send(.handleError(error as? COSError ?? .unknown(error.localizedDescription)))
}
}
case .cancelUpload(let taskId):
return .run { send in
await uploadService.cancelUpload(taskId: taskId)
await send(.upload(.cancelUpload(taskId)))
}
case .uploadCompleted, .uploadFailed, .updateProgress, .reset:
// Action Reducer
return .none
}
}
/// Action
private func handleConfigurationAction(_ state: inout State, _ action: ConfigurationAction) -> Effect<Action> {
switch action {
case .initializeService(let tokenData):
return .run { send in
do {
try await configurationService.initializeCOSService(with: tokenData)
await send(.configuration(.serviceInitialized))
debugInfoSync("✅ COS 服务初始化成功")
} catch {
await send(.configuration(.setError(error.localizedDescription)))
await send(.handleError(error as? COSError ?? .unknown(error.localizedDescription)))
}
}
case .checkInitializationStatus:
return .run { send in
let isInitialized = await configurationService.isCOSServiceInitialized()
await send(.configuration(.initializationStatusReceived(isInitialized)))
}
case .resetService:
return .run { send in
await configurationService.resetCOSService()
await send(.configuration(.serviceReset))
debugInfoSync("🔄 COS 服务已重置")
}
case .serviceInitialized, .initializationStatusReceived, .serviceReset, .setError:
// Action Reducer
return .none
}
}
}
// MARK: - Token State & Action
/// Token
public struct TokenState: Equatable {
/// Token
public var currentToken: TcTokenData?
///
public var isLoading: Bool = false
/// Token
public var statusMessage: String = ""
///
public var error: String?
public init() {}
}
/// Token Action
public enum TokenAction: Equatable {
/// Token
case getToken
/// Token
case tokenReceived(TcTokenData)
/// Token
case refreshToken
/// Token
case getTokenStatus
/// Token
case tokenStatusReceived(String)
/// Token
case clearToken
///
case setError(String?)
}
// MARK: - Upload State & Action
///
public struct UploadState: Equatable {
///
public var currentTask: UploadTask?
///
public var progress: Double = 0.0
///
public var result: String?
///
public var error: String?
///
public var isUploading: Bool = false
public init() {}
}
/// Action
public enum UploadAction: Equatable {
///
case uploadImage(Data, String)
/// UIImage
case uploadUIImage(UIImage, String)
///
case uploadCompleted(String)
///
case uploadFailed(String)
///
case updateProgress(Double)
///
case cancelUpload(UUID)
///
case reset
}
// MARK: - Configuration State & Action
///
public struct ConfigurationState: Equatable {
///
public var serviceStatus: COSServiceStatus = .notInitialized
///
public var currentConfiguration: COSConfiguration?
///
public var error: String?
public init() {}
}
/// Action
public enum ConfigurationAction: Equatable {
///
case initializeService(TcTokenData)
///
case serviceInitialized
///
case checkInitializationStatus
///
case initializationStatusReceived(Bool)
///
case resetService
///
case serviceReset
///
case setError(String?)
}
// MARK: - Reducers
/// Token Reducer
public struct TokenReducer: Reducer {
public typealias State = TokenState
public typealias Action = TokenAction
public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .getToken:
state.isLoading = true
state.error = nil
return .none
case .tokenReceived(let token):
state.currentToken = token
state.isLoading = false
state.error = nil
debugInfoSync("✅ Token 获取成功: \(token.bucket)")
return .none
case .refreshToken:
state.isLoading = true
state.error = nil
return .none
case .getTokenStatus:
return .none
case .tokenStatusReceived(let status):
state.statusMessage = status
return .none
case .clearToken:
state.currentToken = nil
state.statusMessage = ""
state.error = nil
return .none
case .setError(let error):
state.error = error
state.isLoading = false
return .none
}
}
}
}
/// Upload Reducer
public struct UploadReducer: Reducer {
public typealias State = UploadState
public typealias Action = UploadAction
public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .uploadImage(let imageData, let fileName):
state.isUploading = true
state.progress = 0.0
state.error = nil
state.result = nil
state.currentTask = UploadTask(
imageData: imageData,
fileName: fileName,
status: .uploading(progress: 0.0)
)
debugInfoSync("🚀 开始上传图片数据: \(fileName), 大小: \(imageData.count) bytes")
return .none
case .uploadUIImage(let image, let fileName):
state.isUploading = true
state.progress = 0.0
state.error = nil
state.result = nil
// UIImage Data
let imageData = image.jpegData(compressionQuality: 0.7) ?? Data()
state.currentTask = UploadTask(
imageData: imageData,
fileName: fileName,
status: .uploading(progress: 0.0)
)
debugInfoSync("🚀 开始上传UIImage: \(fileName), 大小: \(imageData.count) bytes")
return .none
case .uploadCompleted(let url):
state.isUploading = false
state.progress = 1.0
state.result = url
state.error = nil
state.currentTask = state.currentTask?.updatingStatus(.success(url: url))
debugInfoSync("✅ 上传完成: \(url)")
return .none
case .uploadFailed(let error):
state.isUploading = false
state.error = error
state.currentTask = state.currentTask?.updatingStatus(.failure(error: error))
debugErrorSync("❌ 上传失败: \(error)")
return .none
case .updateProgress(let progress):
state.progress = progress
state.currentTask = state.currentTask?.updatingStatus(.uploading(progress: progress))
return .none
case .cancelUpload:
state.isUploading = false
state.error = "上传已取消"
state.currentTask = state.currentTask?.updatingStatus(.failure(error: "上传已取消"))
debugInfoSync("❌ 上传已取消")
return .none
case .reset:
state.currentTask = nil
state.progress = 0.0
state.result = nil
state.error = nil
state.isUploading = false
return .none
}
}
}
}
/// Configuration Reducer
public struct ConfigurationReducer: Reducer {
public typealias State = ConfigurationState
public typealias Action = ConfigurationAction
public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .initializeService(let tokenData):
state.serviceStatus = .initializing
state.error = nil
state.currentConfiguration = COSConfiguration(
region: tokenData.region,
bucket: tokenData.bucket
)
debugInfoSync("🔄 开始初始化 COS 服务: \(tokenData.bucket)")
return .none
case .serviceInitialized:
state.serviceStatus =
.initialized(
configuration: state.currentConfiguration ?? COSConfiguration(
region: "ap-hongkong",
bucket: "molistar-1320554189"
)
)
debugInfoSync("✅ COS 服务初始化成功")
return .none
case .checkInitializationStatus:
return .none
case .initializationStatusReceived(let isInitialized):
if isInitialized {
state.serviceStatus =
.initialized(
configuration: state.currentConfiguration ?? COSConfiguration(
region: "ap-hongkong",
bucket: "molistar-1320554189"
)
)
} else {
state.serviceStatus = .notInitialized
}
return .none
case .resetService:
state.serviceStatus = .notInitialized
state.currentConfiguration = nil
state.error = nil
debugInfoSync("🔄 COS 服务已重置")
return .none
case .serviceReset:
state.serviceStatus = .notInitialized
state.currentConfiguration = nil
return .none
case .setError(let error):
state.error = error
state.serviceStatus = .failed(error: error ?? "未知错误")
return .none
}
}
}
}