
- 修改COSManagerAdapter以支持新的TCCos组件,确保与腾讯云COS的兼容性。 - 在CreateFeedFeature中新增图片上传相关状态和Action,优化图片选择与上传逻辑。 - 更新CreateFeedView以整合图片上传功能,提升用户体验。 - 在多个视图中添加键盘状态管理,改善用户交互体验。 - 新增COS相关的测试文件,确保功能的正确性和稳定性。
616 lines
21 KiB
Swift
616 lines
21 KiB
Swift
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
|
||
}
|
||
}
|
||
}
|
||
}
|