259 lines
10 KiB
Swift
259 lines
10 KiB
Swift
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?()
|
||
}
|
||
}
|
||
}
|
||
} |