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

417 lines
13 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 SwiftUI
import ComposableArchitecture
import PhotosUI
// MARK: - COS
/// COS
///
public struct COSUploadView: View {
// MARK: - Properties
let store: StoreOf<COSFeature>
@State private var selectedImage: UIImage?
@State private var showingImagePicker = false
// MARK: - Initialization
public init(store: StoreOf<COSFeature>) {
self.store = store
}
// MARK: - Body
public var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
VStack(spacing: 20) {
//
ImageSelectionArea(
selectedImage: $selectedImage,
showingImagePicker: $showingImagePicker
)
//
if let uploadState = viewStore.uploadState,
uploadState.isUploading || uploadState.result != nil || uploadState.error != nil {
UploadProgressArea(uploadState: uploadState)
}
//
UploadButton(
selectedImage: selectedImage,
isUploading: viewStore.uploadState?.isUploading == true,
isServiceReady: isServiceReady(viewStore),
onUpload: {
uploadImage(viewStore)
}
)
Spacer()
}
.padding()
.sheet(isPresented: $showingImagePicker) {
ImagePicker(selectedImage: $selectedImage)
}
}
}
// MARK: -
///
private func isServiceReady(_ viewStore: ViewStore<COSFeature.State, COSFeature.Action>) -> Bool {
let isInitialized = viewStore.configurationState?.serviceStatus.isInitialized == true
let hasValidToken = viewStore.tokenState?.currentToken?.isValid == true
return isInitialized && hasValidToken
}
///
private func uploadImage(_ viewStore: ViewStore<COSFeature.State, COSFeature.Action>) {
guard let image = selectedImage else { return }
let fileName = "image_\(Date().timeIntervalSince1970).jpg"
viewStore.send(.upload(.uploadUIImage(image, fileName)))
}
}
// MARK: -
///
private struct ImageSelectionArea: View {
@Binding var selectedImage: UIImage?
@Binding var showingImagePicker: Bool
var body: some View {
VStack(spacing: 16) {
if let image = selectedImage {
//
VStack(spacing: 12) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxHeight: 200)
.cornerRadius(8)
HStack(spacing: 12) {
Button("重新选择") {
showingImagePicker = true
}
.buttonStyle(.bordered)
Button("清除") {
selectedImage = nil
}
.buttonStyle(.bordered)
.foregroundColor(.red)
}
}
} else {
//
VStack(spacing: 12) {
Image(systemName: "photo.badge.plus")
.font(.system(size: 48))
.foregroundColor(.blue)
Text("选择图片")
.font(.headline)
Text("点击选择要上传的图片")
.font(.caption)
.foregroundColor(.secondary)
Button("选择图片") {
showingImagePicker = true
}
.buttonStyle(.borderedProminent)
}
.frame(maxWidth: .infinity)
.padding(40)
.background(Color(.systemGray6))
.cornerRadius(12)
}
}
}
}
// MARK: -
///
private struct UploadProgressArea: View {
let uploadState: UploadState
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: progressIcon)
.foregroundColor(progressColor)
Text("上传进度")
.font(.headline)
Spacer()
if uploadState.isUploading {
Button("取消") {
// TODO:
}
.font(.caption)
.foregroundColor(.red)
}
}
if let task = uploadState.currentTask {
VStack(alignment: .leading, spacing: 4) {
Text("文件: \(task.fileName)")
.font(.caption)
.foregroundColor(.secondary)
Text("大小: \(ByteCountFormatter.string(fromByteCount: Int64(task.imageData.count), countStyle: .file))")
.font(.caption)
.foregroundColor(.secondary)
}
}
if uploadState.isUploading {
VStack(spacing: 8) {
ProgressView(value: uploadState.progress)
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
HStack {
Text("\(Int(uploadState.progress * 100))%")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Text(estimatedTimeRemaining)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
if let result = uploadState.result {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("上传成功")
.font(.headline)
.foregroundColor(.green)
}
Text("URL: \(result)")
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(3)
Button("复制链接") {
UIPasteboard.general.string = result
}
.buttonStyle(.bordered)
.font(.caption)
}
.padding()
.background(Color(.systemGreen).opacity(0.1))
.cornerRadius(8)
}
if let error = uploadState.error {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text("上传失败")
.font(.headline)
.foregroundColor(.red)
}
Text(error)
.font(.caption)
.foregroundColor(.red)
}
.padding()
.background(Color(.systemRed).opacity(0.1))
.cornerRadius(8)
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
}
private var progressIcon: String {
if uploadState.isUploading {
return "arrow.up.circle.fill"
} else if uploadState.result != nil {
return "checkmark.circle.fill"
} else if uploadState.error != nil {
return "xmark.circle.fill"
} else {
return "arrow.up.circle"
}
}
private var progressColor: Color {
if uploadState.isUploading {
return .blue
} else if uploadState.result != nil {
return .green
} else if uploadState.error != nil {
return .red
} else {
return .gray
}
}
private var estimatedTimeRemaining: String {
//
let remainingProgress = 1.0 - uploadState.progress
if remainingProgress > 0 {
let estimatedSeconds = Int(remainingProgress * 30) // 30
return "预计剩余 \(estimatedSeconds)"
} else {
return "即将完成"
}
}
}
// MARK: -
///
private struct UploadButton: View {
let selectedImage: UIImage?
let isUploading: Bool
let isServiceReady: Bool
let onUpload: () -> Void
var body: some View {
VStack(spacing: 12) {
if !isServiceReady {
VStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle")
.foregroundColor(.orange)
Text("服务未就绪")
.font(.headline)
.foregroundColor(.orange)
Text("请确保 Token 有效且服务已初始化")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding()
.background(Color(.systemOrange).opacity(0.1))
.cornerRadius(8)
}
Button(action: onUpload) {
HStack {
if isUploading {
ProgressView()
.scaleEffect(0.8)
.tint(.white)
} else {
Image(systemName: "arrow.up.circle.fill")
}
Text(buttonTitle)
}
.frame(maxWidth: .infinity)
.padding()
}
.buttonStyle(.borderedProminent)
.disabled(selectedImage == nil || isUploading || !isServiceReady)
}
}
private var buttonTitle: String {
if isUploading {
return "上传中..."
} else if selectedImage == nil {
return "请先选择图片"
} else if !isServiceReady {
return "服务未就绪"
} else {
return "开始上传"
}
}
}
// MARK: -
///
private struct ImagePicker: UIViewControllerRepresentable {
@Binding var selectedImage: UIImage?
@Environment(\.presentationMode) var presentationMode
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.filter = .images
configuration.selectionLimit = 1
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
parent.presentationMode.wrappedValue.dismiss()
guard let provider = results.first?.itemProvider else { return }
if provider.canLoadObject(ofClass: UIImage.self) {
provider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in
//
guard let uiImage = image as? UIImage else { return }
// 线
DispatchQueue.main.async {
guard let self = self else { return }
self.parent.selectedImage = uiImage
}
}
}
}
}
}
// MARK: -
extension COSServiceStatus {
var isInitialized: Bool {
switch self {
case .initialized:
return true
default:
return false
}
}
}
// MARK: -
#Preview {
COSUploadView(
store: Store(
initialState: COSFeature.State(),
reducer: { COSFeature() }
)
)
}