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

284 lines
9.8 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 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`)"
}
}