feat: 更新COSManager和相关视图以增强图片上传功能
- 修改COSManagerAdapter以支持新的TCCos组件,确保与腾讯云COS的兼容性。 - 在CreateFeedFeature中新增图片上传相关状态和Action,优化图片选择与上传逻辑。 - 更新CreateFeedView以整合图片上传功能,提升用户体验。 - 在多个视图中添加键盘状态管理,改善用户交互体验。 - 新增COS相关的测试文件,确保功能的正确性和稳定性。
This commit is contained in:
615
yana/Utils/TCCos/Features/COSFeature.swift
Normal file
615
yana/Utils/TCCos/Features/COSFeature.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user