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,283 @@
import Foundation
import QCloudCOSXML
import UIKit
import ComposableArchitecture
// MARK: -
///
public protocol COSUploadServiceProtocol: Sendable {
///
func uploadImage(_ imageData: Data, fileName: String) async throws -> String
/// UIImage
func uploadUIImage(_ image: UIImage, fileName: String) async throws -> String
///
func cancelUpload(taskId: UUID) async
}
// MARK: -
///
public struct COSUploadService: COSUploadServiceProtocol {
private let tokenService: COSTokenServiceProtocol
private let configurationService: COSConfigurationServiceProtocol
private let uploadTaskManager: UploadTaskManagerProtocol
public init(
tokenService: COSTokenServiceProtocol,
configurationService: COSConfigurationServiceProtocol,
uploadTaskManager: UploadTaskManagerProtocol = UploadTaskManager()
) {
self.tokenService = tokenService
self.configurationService = configurationService
self.uploadTaskManager = uploadTaskManager
}
///
public func uploadImage(_ imageData: Data, fileName: String) async throws -> String {
debugInfoSync("🚀 开始上传图片,数据大小: \(imageData.count) bytes")
// Token
let tokenData = try await tokenService.getValidToken()
// COS
try await configurationService.initializeCOSService(with: tokenData)
//
let task = UploadTask(
imageData: imageData,
fileName: fileName
)
//
return try await performUpload(task: task, tokenData: tokenData)
}
/// UIImage
public func uploadUIImage(_ image: UIImage, fileName: String) async throws -> String {
guard let data = image.jpegData(compressionQuality: 0.7) else {
throw COSError.uploadFailed("图片压缩失败,无法生成 JPEG 数据")
}
return try await uploadImage(data, fileName: fileName)
}
///
public func cancelUpload(taskId: UUID) async {
await uploadTaskManager.cancelTask(taskId)
}
// MARK: -
///
private func performUpload(task: UploadTask, tokenData: TcTokenData) async throws -> String {
//
await uploadTaskManager.registerTask(task)
return try await withCheckedThrowingContinuation { continuation in
Task {
do {
//
let request = try await createUploadRequest(task: task, tokenData: tokenData)
//
request.sendProcessBlock = { @Sendable (bytesSent, totalBytesSent, totalBytesExpectedToSend) in
Task {
let progress = UploadProgress(
bytesSent: bytesSent,
totalBytesSent: totalBytesSent,
totalBytesExpectedToSend: totalBytesExpectedToSend
)
await self.uploadTaskManager.updateTaskProgress(
taskId: task.id,
progress: progress
)
debugInfoSync("📊 上传进度: \(progress.progress * 100)%")
}
}
//
request.setFinish { @Sendable result, error in
Task {
await self.uploadTaskManager.unregisterTask(task.id)
if let error = error {
debugErrorSync("❌ 图片上传失败: \(error.localizedDescription)")
continuation.resume(throwing: COSError.uploadFailed(error.localizedDescription))
} else {
//
let cloudURL = tokenData.buildCloudURL(for: task.fileName)
debugInfoSync("✅ 图片上传成功: \(cloudURL)")
continuation.resume(returning: cloudURL)
}
}
}
//
QCloudCOSTransferMangerService.defaultCOSTransferManager().uploadObject(request)
} catch {
await uploadTaskManager.unregisterTask(task.id)
continuation.resume(throwing: error)
}
}
}
}
///
private func createUploadRequest(task: UploadTask, tokenData: TcTokenData) async throws -> QCloudCOSXMLUploadObjectRequest<AnyObject> {
//
let credential = QCloudCredential()
credential.secretID = tokenData.secretId
credential.secretKey = tokenData.secretKey.trimmingCharacters(in: .whitespacesAndNewlines)
credential.token = tokenData.sessionToken
credential.startDate = tokenData.startDate
credential.expirationDate = tokenData.expirationDate
//
let request = QCloudCOSXMLUploadObjectRequest<AnyObject>()
request.bucket = tokenData.bucket
request.regionName = tokenData.region
request.credential = credential
request.object = task.fileName
request.body = task.imageData as AnyObject
//
if tokenData.accelerate {
request.enableQuic = true
}
return request
}
}
// MARK: -
///
public protocol UploadTaskManagerProtocol: Sendable {
///
func registerTask(_ task: UploadTask) async
///
func unregisterTask(_ taskId: UUID) async
///
func updateTaskProgress(taskId: UUID, progress: UploadProgress) async
///
func cancelTask(_ taskId: UUID) async
///
func getTaskStatus(_ taskId: UUID) async -> UploadStatus?
}
// MARK: -
///
public actor UploadTaskManager: UploadTaskManagerProtocol {
private var activeTasks: [UUID: UploadTask] = [:]
public init() {}
///
public func registerTask(_ task: UploadTask) async {
activeTasks[task.id] = task
debugInfoSync("📝 注册上传任务: \(task.id)")
}
///
public func unregisterTask(_ taskId: UUID) async {
activeTasks.removeValue(forKey: taskId)
debugInfoSync("📝 注销上传任务: \(taskId)")
}
///
public func updateTaskProgress(taskId: UUID, progress: UploadProgress) async {
guard var task = activeTasks[taskId] else { return }
let newStatus = UploadStatus.uploading(progress: progress.progress)
task = task.updatingStatus(newStatus)
activeTasks[taskId] = task
}
///
public func cancelTask(_ taskId: UUID) async {
guard let task = activeTasks[taskId] else { return }
//
let updatedTask = task.updatingStatus(.failure(error: "任务已取消"))
activeTasks[taskId] = updatedTask
debugInfoSync("❌ 取消上传任务: \(taskId)")
}
///
public func getTaskStatus(_ taskId: UUID) async -> UploadStatus? {
return activeTasks[taskId]?.status
}
}
// MARK: - TCA
extension DependencyValues {
/// COS
public var cosUploadService: COSUploadServiceProtocol {
get { self[COSUploadServiceKey.self] }
set { self[COSUploadServiceKey.self] = newValue }
}
}
/// COS
private enum COSUploadServiceKey: DependencyKey {
static let liveValue: COSUploadServiceProtocol = COSUploadService(
tokenService: COSTokenService(apiService: LiveAPIService()),
configurationService: COSConfigurationService()
)
static let testValue: COSUploadServiceProtocol = MockCOSUploadService()
}
// MARK: - Mock
/// Mock
public struct MockCOSUploadService: COSUploadServiceProtocol {
public var uploadImageResult: Result<String, Error> = .failure(COSError.uploadFailed("Mock error"))
public var uploadUIImageResult: Result<String, Error> = .failure(COSError.uploadFailed("Mock error"))
public init() {}
public func uploadImage(_ imageData: Data, fileName: String) async throws -> String {
switch uploadImageResult {
case .success(let url):
return url
case .failure(let error):
throw error
}
}
public func uploadUIImage(_ image: UIImage, fileName: String) async throws -> String {
switch uploadUIImageResult {
case .success(let url):
return url
case .failure(let error):
throw error
}
}
public func cancelUpload(taskId: UUID) async {
// Mock
}
}
// MARK: -
extension UploadTask {
///
public static func generateFileName(extension: String = "jpg") -> String {
let uuid = UUID().uuidString
return "images/\(uuid).\(`extension`)"
}
}