feat: 添加COSManagerAdapter以支持新的TCCos组件

- 新增COSManagerAdapter类,保持与现有COSManager相同的接口,内部使用新的TCCos组件。
- 实现获取、刷新Token及上传图片的功能,确保与腾讯云COS的兼容性。
- 在CreateFeedView中重构内容输入、图片选择和发布按钮逻辑,提升用户体验。
- 更新EditFeedView以优化视图结构和状态管理,确保功能正常运行。
- 在多个视图中添加键盘状态管理,改善用户交互体验。
This commit is contained in:
edwinQQQ
2025-07-31 11:41:38 +08:00
parent 3d00e459e3
commit beda539e00
5 changed files with 209 additions and 456 deletions

View File

@@ -255,4 +255,4 @@ struct EditFeedFeature {
}
}
}
}
}

View File

@@ -1,241 +0,0 @@
import Foundation
import QCloudCOSXML
// MARK: - COS
/// COS
///
/// COS
/// - Token
/// -
/// -
@MainActor
class COSManager: ObservableObject {
static let shared = COSManager()
private init() {}
//
private static var isCOSInitialized = false
//
private func ensureCOSInitialized(tokenData: TcTokenData) {
guard !Self.isCOSInitialized else { return }
let configuration = QCloudServiceConfiguration()
let endpoint = QCloudCOSXMLEndPoint()
endpoint.regionName = tokenData.region
endpoint.useHTTPS = true
if tokenData.accelerate {
endpoint.suffix = "cos.accelerate.myqcloud.com"
}
configuration.endpoint = endpoint
QCloudCOSXMLService.registerDefaultCOSXML(with: configuration)
QCloudCOSTransferMangerService.registerDefaultCOSTransferManger(with: configuration)
Self.isCOSInitialized = true
debugInfoSync("✅ COS服务已初始化region: \(tokenData.region)")
}
// MARK: - Token
/// Token
private var cachedToken: TcTokenData?
private var tokenExpirationDate: Date?
/// COS Token
/// - Parameter apiService: API
/// - Returns: Token nil
func getToken(apiService: any APIServiceProtocol & Sendable) async -> TcTokenData? {
//
if let cached = cachedToken, let expiration = tokenExpirationDate, Date() < expiration {
debugInfoSync("🔐 使用缓存的 COS Token")
return cached
}
//
clearCachedToken()
// Token
debugInfoSync("🔐 开始请求腾讯云 COS Token...")
do {
let request = TcTokenRequest()
let response: TcTokenResponse = try await apiService.request(request)
guard response.code == 200, let tokenData = response.data else {
debugInfoSync("❌ COS Token 请求失败: \(response.message)")
return nil
}
// Token
cachedToken = tokenData
tokenExpirationDate = tokenData.expirationDate
debugInfoSync("✅ COS Token 获取成功")
debugInfoSync(" - 存储桶: \(tokenData.bucket)")
debugInfoSync(" - 地域: \(tokenData.region)")
debugInfoSync(" - 过期时间: \(tokenData.expirationDate)")
debugInfoSync(" - 剩余时间: \(tokenData.remainingTime)")
return tokenData
} catch {
debugInfoSync("❌ COS Token 请求异常: \(error.localizedDescription)")
return nil
}
}
/// Token
/// - Parameter tokenData: Token
private func cacheToken(_ tokenData: TcTokenData) async {
cachedToken = tokenData
// expiration ISO 8601
if let expirationDate = ISO8601DateFormatter().date(from: String(tokenData.expireTime)) {
// 5
tokenExpirationDate = expirationDate.addingTimeInterval(-300)
} else {
// 1
tokenExpirationDate = Date().addingTimeInterval(3600)
}
debugInfoSync("💾 COS Token 已缓存,过期时间: \(tokenExpirationDate?.description ?? "未知")")
}
/// Token
private func clearCachedToken() {
cachedToken = nil
tokenExpirationDate = nil
debugInfoSync("🗑️ 清除缓存的 COS Token")
}
/// Token
func refreshToken(apiService: any APIServiceProtocol & Sendable) async -> TcTokenData? {
clearCachedToken()
return await getToken(apiService: apiService)
}
// MARK: -
/// 访 Token
var token: TcTokenData? { cachedToken }
// MARK: -
/// Token
func getTokenStatus() -> String {
if let _ = cachedToken, let expiration = tokenExpirationDate {
let isExpired = Date() >= expiration
return "Token 状态: \(isExpired ? "已过期" : "有效"), 过期时间: \(expiration)"
} else {
return "Token 状态: 未缓存"
}
}
// MARK: -
/// COS
/// - Parameters:
/// - imageData:
/// - apiService: API
/// - Returns: nil
func uploadImage(_ imageData: Data, apiService: any APIServiceProtocol & Sendable) async -> String? {
guard let tokenData = await getToken(apiService: apiService) else {
debugInfoSync("❌ 无法获取 COS Token")
return nil
}
// COS
ensureCOSInitialized(tokenData: tokenData)
// COS
let credential = QCloudCredential()
credential.secretID = tokenData.secretId
// secretKey
let rawSecretKey = tokenData.secretKey.trimmingCharacters(in: .whitespacesAndNewlines)
debugInfoSync("secretKey原始内容: [\(rawSecretKey)]")
credential.secretKey = rawSecretKey
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
// key
let fileExtension = "jpg" // JPG
let key = "images/\(UUID().uuidString).\(fileExtension)"
request.object = key
request.body = imageData as AnyObject
//
request.sendProcessBlock = { (bytesSent, totalBytesSent,
totalBytesExpectedToSend) in
debugInfoSync("\(bytesSent), \(totalBytesSent), \(totalBytesExpectedToSend)")
// bytesSent
// totalBytesSent
// totalBytesExpectedToSend
};
//
if tokenData.accelerate {
request.enableQuic = true
// endpoint "cos.accelerate.myqcloud.com"
}
// 使 async/await
return await withCheckedContinuation { continuation in
request.setFinish { result, error in
if let error = error {
debugInfoSync("❌ 图片上传失败: \(error.localizedDescription)")
continuation.resume(returning: " ?????????? ")
} else {
//
let domain = tokenData.customDomain.isEmpty ? "\(tokenData.bucket).cos.\(tokenData.region).myqcloud.com" : tokenData.customDomain
let prefix = domain.hasPrefix("http") ? "" : "https://"
let cloudURL = "\(prefix)\(domain)/\(key)"
debugInfoSync("✅ 图片上传成功: \(cloudURL)")
continuation.resume(returning: cloudURL)
}
}
QCloudCOSTransferMangerService.defaultCOSTransferManager().uploadObject(request)
}
}
/// UIImage COS JPEG(0.8)
/// - Parameters:
/// - image: UIImage
/// - apiService: API
/// - Returns: nil
func uploadUIImage(_ image: UIImage, apiService: any APIServiceProtocol & Sendable) async -> String? {
guard let data = image.jpegData(compressionQuality: 0.8) else {
debugInfoSync("❌ 图片压缩失败,无法生成 JPEG 数据")
return nil
}
return await uploadImage(data, apiService: apiService)
}
}
// MARK: -
extension COSManager {
/// Token
func testTokenRetrieval(apiService: any APIServiceProtocol & Sendable) async {
#if DEBUG
debugInfoSync("\n🧪 开始测试腾讯云 COS Token 获取功能")
let token = await getToken(apiService: apiService)
if let tokenData = token {
debugInfoSync("✅ Token 获取成功")
debugInfoSync(" bucket: \(tokenData.bucket)")
debugInfoSync(" Expiration: \(tokenData.expireTime)")
debugInfoSync(" Token: \(tokenData.sessionToken.prefix(20))...")
debugInfoSync(" SecretId: \(tokenData.secretId.prefix(20))...")
} else {
debugInfoSync("❌ Token 获取失败")
}
debugInfoSync("📊 Token 状态: \(getTokenStatus())")
debugInfoSync("✅ 腾讯云 COS Token 测试完成\n")
#endif
}
}

View File

@@ -0,0 +1,8 @@
//
// COSManagerAdapter.swift
// yana
//
// Created by P on 2025/7/31.
//
import Foundation

View File

@@ -174,66 +174,64 @@ struct CreateFeedView: View {
}
// MARK: - iOS 16+
//struct ModernImageSelectionGrid: View {
// let images: [UIImage]
// let selectedItems: [PhotosPickerItem]
// let canAddMore: Bool
// let onItemsChanged: ([PhotosPickerItem]) -> Void
// let onRemoveImage: (Int) -> Void
//
// private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
//
// var body: some View {
// WithPerceptionTracking {
// LazyVGrid(columns: columns, spacing: 8) {
// //
// ForEach(Array(images.enumerated()), id: \.offset) { index, image in
// ZStack(alignment: .topTrailing) {
// Image(uiImage: image)
// .resizable()
// .aspectRatio(contentMode: .fill)
// .frame(height: 100)
// .clipped()
// .cornerRadius(8)
//
// //
// Button(action: {
// onRemoveImage(index)
// }) {
// Image(systemName: "xmark.circle.fill")
// .font(.system(size: 20))
// .foregroundColor(.white)
// .background(Color.black.opacity(0.6))
// .clipShape(Circle())
// }
// .padding(4)
// }
// }
//
// //
// if canAddMore {
// PhotosPicker(
// selection: .init(
// get: { selectedItems },
// set: onItemsChanged
// ),
// maxSelectionCount: 9,
// matching: .images
// ) {
// RoundedRectangle(cornerRadius: 8)
// .fill(Color.white.opacity(0.1))
// .frame(height: 100)
// .overlay(
// Image(systemName: "plus")
// .font(.system(size: 40))
// .foregroundColor(.white.opacity(0.6))
// )
// }
// }
// }
// }
// }
//}
struct ModernImageSelectionGrid: View {
let images: [UIImage]
let selectedItems: [PhotosPickerItem]
let canAddMore: Bool
let onItemsChanged: ([PhotosPickerItem]) -> Void
let onRemoveImage: (Int) -> Void
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
var body: some View {
LazyVGrid(columns: columns, spacing: 8) {
//
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
ZStack(alignment: .topTrailing) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(height: 100)
.clipped()
.cornerRadius(8)
//
Button(action: {
onRemoveImage(index)
}) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 20))
.foregroundColor(.white)
.background(Color.black.opacity(0.6))
.clipShape(Circle())
}
.padding(4)
}
}
//
if canAddMore {
PhotosPicker(
selection: .init(
get: { selectedItems },
set: onItemsChanged
),
maxSelectionCount: 9,
matching: .images
) {
RoundedRectangle(cornerRadius: 8)
.fill(Color.white.opacity(0.1))
.frame(height: 100)
.overlay(
Image(systemName: "plus")
.font(.system(size: 40))
.foregroundColor(.white.opacity(0.6))
)
}
}
}
}
}
// MARK: -
//#Preview {

View File

@@ -13,70 +13,68 @@ struct EditFeedView: View {
}
var body: some View {
WithPerceptionTracking {
GeometryReader { geometry in
ZStack {
backgroundView
mainContent(geometry: geometry)
if store.isUploadingImages {
uploadingImagesOverlay(progress: store.imageUploadProgress)
} else if store.isLoading {
loadingOverlay
}
}
.contentShape(Rectangle())
.onTapGesture {
if isKeyboardVisible {
hideKeyboard()
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
withAnimation(.easeInOut(duration: 0.3)) {
isKeyboardVisible = true
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
withAnimation(.easeInOut(duration: 0.3)) {
isKeyboardVisible = false
}
GeometryReader { geometry in
ZStack {
backgroundView
mainContent(geometry: geometry)
if store.isUploadingImages {
uploadingImagesOverlay(progress: store.imageUploadProgress)
} else if store.isLoading {
loadingOverlay
}
}
.navigationBarHidden(true)
.onAppear {
store.send(.clearError)
}
.onChange(of: store.shouldDismiss) {
if store.shouldDismiss {
onDismiss()
.contentShape(Rectangle())
.onTapGesture {
if isKeyboardVisible {
hideKeyboard()
}
}
.photosPicker(
isPresented: Binding(
get: { store.showPhotosPicker },
set: { _ in store.send(.photosPickerDismissed) }
),
selection: Binding(
get: { store.selectedPhotoItems },
set: { store.send(.photosPickerItemsChanged($0)) }
),
maxSelectionCount: 9,
matching: .images
)
.alert("删除图片", isPresented: Binding(
get: { store.showDeleteImageAlert },
set: { _ in store.send(.deleteImageAlertDismissed) }
)) {
Button("删除", role: .destructive) {
if let indexToDelete = store.imageToDeleteIndex {
store.send(.removeImage(indexToDelete))
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
withAnimation(.easeInOut(duration: 0.3)) {
isKeyboardVisible = true
}
Button("取消", role: .cancel) {
store.send(.deleteImageAlertDismissed)
}
} message: {
Text("确定要删除这张图片吗?")
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
withAnimation(.easeInOut(duration: 0.3)) {
isKeyboardVisible = false
}
}
}
.navigationBarHidden(true)
.onAppear {
store.send(.clearError)
}
.onChange(of: store.shouldDismiss) {
if store.shouldDismiss {
onDismiss()
}
}
.photosPicker(
isPresented: Binding(
get: { store.showPhotosPicker },
set: { _ in store.send(.photosPickerDismissed) }
),
selection: Binding(
get: { store.selectedPhotoItems },
set: { store.send(.photosPickerItemsChanged($0)) }
),
maxSelectionCount: 9,
matching: .images
)
.alert("删除图片", isPresented: Binding(
get: { store.showDeleteImageAlert },
set: { _ in store.send(.deleteImageAlertDismissed) }
)) {
Button("删除", role: .destructive) {
if let indexToDelete = store.imageToDeleteIndex {
store.send(.removeImage(indexToDelete))
}
}
Button("取消", role: .cancel) {
store.send(.deleteImageAlertDismissed)
}
} message: {
Text("确定要删除这张图片吗?")
}
}
@@ -86,127 +84,117 @@ struct EditFeedView: View {
}
private func mainContent(geometry: GeometryProxy) -> some View {
WithPerceptionTracking {
VStack(spacing: 0) {
topNavigationBar
ScrollView {
VStack(spacing: 20) {
textInputSection
imageSelectionSection
publishButton
}
.padding(.horizontal, 20)
.padding(.bottom, 40)
VStack(spacing: 0) {
topNavigationBar
ScrollView {
VStack(spacing: 20) {
textInputSection
imageSelectionSection
publishButton
}
.padding(.horizontal, 20)
.padding(.bottom, 40)
}
}
}
private var topNavigationBar: some View {
WithPerceptionTracking {
HStack {
Button(action: {
store.send(.clearDismissFlag)
onDismiss()
}) {
Image(systemName: "xmark")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
}
Spacer()
Text("编辑动态")
.font(.system(size: 18, weight: .medium))
HStack {
Button(action: {
store.send(.clearDismissFlag)
onDismiss()
}) {
Image(systemName: "xmark")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
Spacer()
Color.clear
.frame(width: 44, height: 44)
}
.padding(.horizontal, 16)
.padding(.top, 8)
.padding(.bottom, 16)
Spacer()
Text("编辑动态")
.font(.system(size: 18, weight: .medium))
.foregroundColor(.white)
Spacer()
Color.clear
.frame(width: 44, height: 44)
}
.padding(.horizontal, 16)
.padding(.top, 8)
.padding(.bottom, 16)
}
private var textInputSection: some View {
WithPerceptionTracking {
VStack(alignment: .leading, spacing: 12) {
Text("分享你的想法...")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
TextEditor(text: Binding(
get: { store.content },
set: { store.send(.contentChanged($0)) }
))
.font(.system(size: 16))
VStack(alignment: .leading, spacing: 12) {
Text("分享你的想法...")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
.background(Color.clear)
.frame(minHeight: 120)
.padding(12)
.background(Color.white.opacity(0.1))
.cornerRadius(12)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.white.opacity(0.2), lineWidth: 1)
)
HStack {
Spacer()
Text("\(store.content.count)/\(maxCount)")
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.6))
}
TextEditor(text: Binding(
get: { store.content },
set: { store.send(.contentChanged($0)) }
))
.font(.system(size: 16))
.foregroundColor(.white)
.background(Color.clear)
.frame(minHeight: 120)
.padding(12)
.background(Color.white.opacity(0.1))
.cornerRadius(12)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.white.opacity(0.2), lineWidth: 1)
)
HStack {
Spacer()
Text("\(store.content.count)/\(maxCount)")
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.6))
}
}
}
private var imageSelectionSection: some View {
WithPerceptionTracking {
VStack(alignment: .leading, spacing: 12) {
Text("添加图片")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
ImageGrid(
images: store.processedImages,
onRemoveImage: { index in
store.send(.showDeleteImageAlert(index))
},
onAddImage: {
store.send(.addImageButtonTapped)
}
)
}
VStack(alignment: .leading, spacing: 12) {
Text("添加图片")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
ImageGrid(
images: store.processedImages,
onRemoveImage: { index in
store.send(.showDeleteImageAlert(index))
},
onAddImage: {
store.send(.addImageButtonTapped)
}
)
}
}
private var publishButton: some View {
WithPerceptionTracking {
Button(action: {
store.send(.publishButtonTapped)
}) {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.2)
} else {
Text("发布")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
}
Button(action: {
store.send(.publishButtonTapped)
}) {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.2)
} else {
Text("发布")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(store.content.isEmpty ? Color.gray : Color.blue)
.cornerRadius(12)
.disabled(store.isLoading || store.content.isEmpty)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(store.content.isEmpty ? Color.gray : Color.blue)
.cornerRadius(12)
.disabled(store.isLoading || store.content.isEmpty)
}
private func uploadingImagesOverlay(progress: Double) -> some View {