wake-ios/wake/View/Components/Upload/MultiImageUploader.swift
2025-08-21 19:39:19 +08:00

259 lines
10 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 PhotosUI
import os.log
@available(iOS 16.0, *)
public struct MultiImageUploader: View {
@State private var selectedImages: [UIImage] = []
@State private var uploadResults: [UploadResult] = []
@State private var isUploading = false
@State private var showingImagePicker = false
@State private var uploadProgress: [UUID: Double] = [:] //
private let maxSelection: Int
private let onUploadComplete: ([UploadResult]) -> Void
private let uploadService = ImageUploadService.shared
private let logger = Logger(subsystem: "com.yourapp.uploader", category: "MultiImageUploader")
public init(
maxSelection: Int = 10,
onUploadComplete: @escaping ([UploadResult]) -> Void
) {
self.maxSelection = maxSelection
self.onUploadComplete = onUploadComplete
}
public var body: some View {
VStack(spacing: 16) {
//
Button(action: {
showingImagePicker = true
}) {
Label("选择图片", systemImage: "photo.on.rectangle")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.cornerRadius(10)
}
.padding(.horizontal)
.sheet(isPresented: $showingImagePicker) {
ImagePicker(images: $selectedImages, selectionLimit: maxSelection) {
//
if !selectedImages.isEmpty {
uploadImages()
}
}
}
//
ScrollView {
LazyVStack(spacing: 16) {
ForEach($uploadResults) { $result in
VStack(spacing: 8) {
//
Image(uiImage: result.image)
.resizable()
.scaledToFill()
.frame(height: 200)
.frame(maxWidth: .infinity)
.clipped()
.cornerRadius(8)
//
if case .uploading = result.status {
ProgressView(value: uploadProgress[result.id] ?? 0, total: 1.0)
.progressViewStyle(LinearProgressViewStyle())
.padding(.horizontal)
Text("上传中: \(Int((uploadProgress[result.id] ?? 0) * 100))%")
.font(.caption)
.foregroundColor(.secondary)
} else if case .success = result.status {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("上传成功")
.font(.caption)
.foregroundColor(.green)
}
} else if case .failure = result.status {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text("上传失败")
.font(.caption)
.foregroundColor(.red)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
}
.padding()
}
//
if !selectedImages.isEmpty && !isUploading {
Button(action: uploadImages) {
Text("开始上传 (\(selectedImages.count)张)")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.cornerRadius(10)
}
.padding(.horizontal)
.padding(.bottom)
}
}
.background(Color(.systemGroupedBackground))
}
private func uploadImages() {
guard !isUploading else { return }
isUploading = true
let group = DispatchGroup()
var results = selectedImages.map { image in
UploadResult(fileId: "", previewFileId: "", image: image, status: .uploading(progress: 0))
}
// UI
uploadResults = results
// DispatchQueue
let uploadQueue = DispatchQueue(label: "com.wake.uploadQueue", attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 3) // 3
for (index, image) in selectedImages.enumerated() {
semaphore.wait()
group.enter()
uploadQueue.async {
let resultId = results[index].id
//
DispatchQueue.main.async {
if let idx = results.firstIndex(where: { $0.id == resultId }) {
results[idx].status = .uploading(progress: 0)
uploadProgress[resultId] = 0
uploadResults = results
}
}
//
uploadService.uploadImage(image, progress: { progress in
DispatchQueue.main.async {
uploadProgress[resultId] = progress.progress
if let idx = results.firstIndex(where: { $0.id == resultId }) {
results[idx].status = .uploading(progress: progress.progress)
uploadResults = results
}
}
}) { uploadResult in
defer {
semaphore.signal()
group.leave()
}
DispatchQueue.main.async {
if let idx = results.firstIndex(where: { $0.id == resultId }) {
switch uploadResult {
case .success(let uploadResult):
//
results[idx].fileId = uploadResult.fileId
// 使 previewFileId ImageUploaderGetID.UploadResult
results[idx].previewFileId = ""
results[idx].status = .success
uploadResults = results
logger.info("图片上传成功: \(uploadResult.fileId)")
case .failure(let error):
//
results[idx].status = .failure(error)
uploadResults = results
logger.error("图片上传失败: \(error.localizedDescription)")
}
}
}
}
}
}
//
group.notify(queue: .main) {
self.isUploading = false
let successCount = results.filter { $0.status == .success }.count
logger.info("上传完成,成功: \(successCount)/\(results.count)")
self.onUploadComplete(results)
}
}
}
// MARK: -
@available(iOS 14.0, *)
struct ImagePicker: UIViewControllerRepresentable {
@Binding var images: [UIImage]
var selectionLimit: Int = 10
var onDismiss: (() -> Void)?
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration(photoLibrary: .shared())
config.filter = .images
config.selectionLimit = selectionLimit
config.preferredAssetRepresentationMode = .current
let picker = PHPickerViewController(configuration: config)
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]) {
picker.dismiss(animated: true)
let group = DispatchGroup()
var newImages: [UIImage] = []
for result in results {
group.enter()
let itemProvider = result.itemProvider
if itemProvider.canLoadObject(ofClass: UIImage.self) {
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in
if let image = image as? UIImage {
newImages.append(image)
}
group.leave()
}
} else {
group.leave()
}
}
group.notify(queue: .main) {
self.parent.images = newImages
self.parent.onDismiss?()
}
}
}
}