Files
e-party-iOS/yana/Utils/TCCos/Features/COSFeature.swift
edwinQQQ b966e24532 feat: 更新COSManager和相关视图以增强图片上传功能
- 修改COSManagerAdapter以支持新的TCCos组件,确保与腾讯云COS的兼容性。
- 在CreateFeedFeature中新增图片上传相关状态和Action,优化图片选择与上传逻辑。
- 更新CreateFeedView以整合图片上传功能,提升用户体验。
- 在多个视图中添加键盘状态管理,改善用户交互体验。
- 新增COS相关的测试文件,确保功能的正确性和稳定性。
2025-07-31 11:41:56 +08:00

616 lines
21 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}
}
}