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