diff --git a/.DS_Store b/.DS_Store index 9d48cbe..b7b5d2b 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate index 115a7fb..69ed377 100644 Binary files a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate and b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/wake/CoreData/.DS_Store b/wake/CoreData/.DS_Store new file mode 100644 index 0000000..57909a7 Binary files /dev/null and b/wake/CoreData/.DS_Store differ diff --git a/wake/View/Components/Upload/ImagePicker.swift b/wake/View/Components/Upload/ImagePicker.swift new file mode 100644 index 0000000..cb806dc --- /dev/null +++ b/wake/View/Components/Upload/ImagePicker.swift @@ -0,0 +1,58 @@ +import SwiftUI +import PhotosUI + +struct ImagePicker: UIViewControllerRepresentable { + @Binding var images: [UIImage] + var selectionLimit: Int + var onDismiss: (() -> Void)? + + func makeUIViewController(context: Context) -> PHPickerViewController { + var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) + configuration.filter = .images + configuration.selectionLimit = selectionLimit + + 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]) { + let group = DispatchGroup() + var newImages: [UIImage] = [] + + for result in results { + group.enter() + + if result.itemProvider.canLoadObject(ofClass: UIImage.self) { + result.itemProvider.loadObject(ofClass: UIImage.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?() + picker.dismiss(animated: true) + } + } + } +} diff --git a/wake/View/Components/Upload/MultiImageUploader.swift b/wake/View/Components/Upload/MultiImageUploader.swift index 3598ef4..788ea11 100644 --- a/wake/View/Components/Upload/MultiImageUploader.swift +++ b/wake/View/Components/Upload/MultiImageUploader.swift @@ -1,258 +1,175 @@ import SwiftUI import PhotosUI -import os.log +import os @available(iOS 16.0, *) -public struct MultiImageUploader: View { - @State private var selectedImages: [UIImage] = [] +public struct MultiImageUploader: View { + @State var selectedImages: [UIImage] = [] @State private var uploadResults: [UploadResult] = [] @State private var isUploading = false @State private var showingImagePicker = false @State private var uploadProgress: [UUID: Double] = [:] // 跟踪每个上传任务的进度 + @State private var needsViewUpdate = false // Add this line to trigger view updates private let maxSelection: Int - private let onUploadComplete: ([UploadResult]) -> Void + public var onUploadComplete: ([UploadResult]) -> Void private let uploadService = ImageUploadService.shared private let logger = Logger(subsystem: "com.yourapp.uploader", category: "MultiImageUploader") + // 自定义内容 + private let content: ((_ isUploading: Bool, _ selectedCount: Int) -> Content)? + + /// 控制是否显示图片选择器 + @Binding var isImagePickerPresented: Bool + + /// 选中的图片 + @Binding var selectedImagesBinding: [UIImage]? + + /// 控制是否显示图片预览 + @State private var showingImagePreview = false + + // 初始化方法 - 使用自定义视图 public init( maxSelection: Int = 10, + isImagePickerPresented: Binding, + selectedImagesBinding: Binding<[UIImage]?>, + @ViewBuilder content: @escaping (_ isUploading: Bool, _ selectedCount: Int) -> Content, onUploadComplete: @escaping ([UploadResult]) -> Void ) { self.maxSelection = maxSelection + self._isImagePickerPresented = isImagePickerPresented + self._selectedImagesBinding = selectedImagesBinding + self.content = content + self.onUploadComplete = onUploadComplete + } + + // 初始化方法 - 使用默认按钮样式(向后兼容) + public init( + maxSelection: Int = 10, + isImagePickerPresented: Binding, + selectedImagesBinding: Binding<[UIImage]?>, + onUploadComplete: @escaping ([UploadResult]) -> Void + ) where Content == EmptyView { + self.maxSelection = maxSelection + self._isImagePickerPresented = isImagePickerPresented + self._selectedImagesBinding = selectedImagesBinding + self.content = nil self.onUploadComplete = onUploadComplete } public var body: some View { VStack(spacing: 16) { - // 上传按钮 - Button(action: { - showingImagePicker = true - }) { - Label("选择图片", systemImage: "photo.on.rectangle") + // 自定义内容或默认按钮 + if let content = content { + Button(action: { + showingImagePicker = true + }) { + content(isUploading, selectedImages.count) + } + .buttonStyle(PlainButtonStyle()) + } else { + // 默认按钮样式 + Button(action: { + showingImagePicker = true + }) { + Label( + !selectedImages.isEmpty ? + "已选择 \(selectedImages.count) 张图片" : + "选择图片", + 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)) + .sheet(isPresented: $showingImagePicker) { + ImagePicker(images: $selectedImages, selectionLimit: maxSelection) { + // 当选择完成时,关闭选择器并开始上传 + showingImagePicker = false + if !selectedImages.isEmpty { + Task { + _ = await startUpload() + } + } + } + } + .onChange(of: isImagePickerPresented) { newValue in + if newValue { + showingImagePicker = true + } + } + .onChange(of: showingImagePicker) { newValue in + if !newValue { + isImagePickerPresented = false + } + } + .onChange(of: selectedImages) { newValue in + selectedImagesBinding = newValue + } + .onChange(of: needsViewUpdate) { _ in + // Trigger view update + } } - private func uploadImages() { - guard !isUploading else { return } + /// 上传图片方法,由父组件调用 + @MainActor + public func startUpload() async -> [UploadResult] { + guard !isUploading && !selectedImages.isEmpty else { return [] } isUploading = true - let group = DispatchGroup() - var results = selectedImages.map { image in - UploadResult(fileId: "", previewFileId: "", image: image, status: .uploading(progress: 0)) + uploadResults = selectedImages.map { + UploadResult(image: $0, status: .uploading(progress: 0)) } - // 更新UI显示上传中的状态 - uploadResults = results - - // 创建并发的DispatchQueue - let uploadQueue = DispatchQueue(label: "com.wake.uploadQueue", attributes: .concurrent) - let semaphore = DispatchSemaphore(value: 3) // 限制并发数为3 + let group = DispatchGroup() 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 + // 使用 ImageUploadService 上传图片 + uploadService.uploadOriginalAndCompressedImage( + image, + compressionQuality: 0.7, + 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 + if index < self.uploadResults.count { + self.uploadResults[index].status = .uploading(progress: progress.progress) + self.needsViewUpdate.toggle() // Trigger view update } } - }) { uploadResult in - defer { - semaphore.signal() - group.leave() - } - + }, + completion: { result in 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) + guard index < self.uploadResults.count else { return } + switch result { + case .success(let uploadResults): + self.uploadResults[index].status = .success + self.uploadResults[index].fileId = uploadResults.original.fileId + self.uploadResults[index].previewFileId = uploadResults.compressed.fileId + self.needsViewUpdate.toggle() // Trigger view update + case .failure(let error): + self.uploadResults[index].status = .failure(error) + self.needsViewUpdate.toggle() // Trigger view update + self.logger.error("图片上传失败: \(error.localizedDescription)") } group.leave() } - } else { - group.leave() } - } - + ) + } + + return await withCheckedContinuation { continuation in group.notify(queue: .main) { - self.parent.images = newImages - self.parent.onDismiss?() + self.isUploading = false + self.needsViewUpdate.toggle() // Trigger view update + continuation.resume(returning: self.uploadResults) } } } diff --git a/wake/View/Examples/ImageUploadExampleView.swift b/wake/View/Examples/ImageUploadExampleView.swift deleted file mode 100644 index 37393de..0000000 --- a/wake/View/Examples/ImageUploadExampleView.swift +++ /dev/null @@ -1,284 +0,0 @@ -import SwiftUI -import os.log - -/// 多图上传示例视图 -/// 展示如何使用 MultiImageUploader 组件实现多图上传功能 -@available(iOS 16.0, *) -struct MultiImageUploadExampleView: View { - // MARK: - 状态属性 - - @State private var uploadResults: [UploadResult] = [] - @State private var isShowingUploader = false - @State private var showUploadAlert = false - @State private var alertMessage = "" - @State private var isUploading = false - - private let logger = Logger(subsystem: "com.yourapp.uploader", category: "MultiImageUploadExample") - - // MARK: - 视图主体 - - var body: some View { - NavigationView { - VStack(spacing: 16) { - // 上传按钮和状态 - uploadButton - - // 上传统计信息 - if !uploadResults.isEmpty { - uploadStatsView - } - - // 上传进度列表 - uploadProgressList - - Spacer() - } - .navigationTitle("多图上传示例") - .toolbar { - // 清空按钮 - if !uploadResults.isEmpty && !isUploading { - Button("清空") { - withAnimation { - uploadResults.removeAll() - } - } - } - } - .sheet(isPresented: $isShowingUploader) { - // 多图上传组件 - MultiImageUploader( - maxSelection: 10 - ) { results in - processUploadResults(results) - } - } - .alert("上传结果", isPresented: $showUploadAlert) { - Button("确定", role: .cancel) { } - } message: { - Text(alertMessage) - } - } - } - - // MARK: - 子视图 - - /// 上传按钮 - private var uploadButton: some View { - Button(action: { isShowingUploader = true }) { - Label("选择并上传图片", systemImage: "photo.on.rectangle.angled") - .font(.headline) - .foregroundColor(.white) - .padding() - .frame(maxWidth: .infinity) - .background(Color.blue) - .cornerRadius(10) - } - .padding(.horizontal) - .padding(.top) - } - - /// 上传统计信息 - private var uploadStatsView: some View { - VStack(alignment: .leading, spacing: 8) { - let successCount = uploadResults.filter { $0.status == .success }.count - let inProgressCount = uploadResults.filter { - if case .uploading = $0.status { return true } - return false - }.count - let failedCount = uploadResults.count - successCount - inProgressCount - - Text("上传统计") - .font(.headline) - - HStack { - StatView( - value: "\(uploadResults.count)", - label: "总数量", - color: .blue - ) - - StatView( - value: "\(successCount)", - label: "成功", - color: .green - ) - - StatView( - value: "\(inProgressCount)", - label: "上传中", - color: .orange - ) - - StatView( - value: "\(failedCount)", - label: "失败", - color: .red - ) - } - - // 总进度条 - if inProgressCount > 0 { - let progress = uploadResults.reduce(0.0) { result, uploadResult in - if case .uploading(let progress) = uploadResult.status { - return result + progress - } else if uploadResult.status == .success { - return result + 1.0 - } - return result - } / Double(uploadResults.count) - - VStack(alignment: .leading, spacing: 4) { - Text("总进度: \(Int(progress * 100))%") - .font(.subheadline) - ProgressView(value: progress) - .progressViewStyle(LinearProgressViewStyle(tint: .blue)) - } - .padding(.top, 8) - } - } - .padding() - .background(Color(.systemGray6)) - .cornerRadius(10) - .padding(.horizontal) - } - - /// 上传进度列表 - private var uploadProgressList: some View { - List { - ForEach($uploadResults) { $result in - VStack(alignment: .leading, spacing: 8) { - // 图片缩略图和状态 - HStack { - // 图片缩略图 - Image(uiImage: result.image) - .resizable() - .scaledToFill() - .frame(width: 60, height: 60) - .clipped() - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(borderColor(for: result.status), lineWidth: 1) - ) - - // 状态和进度 - VStack(alignment: .leading, spacing: 4) { - Text("图片 \(result.id.uuidString.prefix(8))...") - .font(.subheadline) - - switch result.status { - case .uploading(let progress): - VStack(alignment: .leading, spacing: 4) { - Text("上传中: \(Int(progress * 100))%") - .font(.caption) - ProgressView(value: progress) - .progressViewStyle(LinearProgressViewStyle()) - } - case .success: - HStack { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.green) - Text("上传成功") - .font(.caption) - .foregroundColor(.green) - } - case .failure(let error): - HStack { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.red) - Text("上传失败: \(error.localizedDescription)") - .font(.caption) - .foregroundColor(.red) - } - case .idle: - Text("等待上传...") - .font(.caption) - .foregroundColor(.secondary) - } - } - - Spacer() - } - .padding(.vertical, 4) - } - .padding(.vertical, 8) - .listRowSeparator(.hidden) - } - } - .listStyle(PlainListStyle()) - .animation(.easeInOut, value: uploadResults) - } - - // MARK: - 辅助方法 - - /// 获取状态对应的边框颜色 - private func borderColor(for status: UploadStatus) -> Color { - switch status { - case .success: return .green - case .failure: return .red - case .uploading: return .blue - case .idle: return .gray - } - } - - /// 处理上传结果 - private func processUploadResults(_ results: [UploadResult]) { - // 更新状态 - isUploading = results.contains { result in - if case .uploading = result.status { return true } - return false - } - - // 更新结果 - withAnimation { - uploadResults = results - isShowingUploader = false - } - - // 检查是否全部完成 - let allFinished = !results.contains { result in - if case .uploading = result.status { return true } - if case .idle = result.status { return true } - return false - } - - if allFinished { - let successCount = results.filter { $0.status == .success }.count - let totalCount = results.count - alertMessage = "上传完成\n成功: \(successCount)/\(totalCount)" - showUploadAlert = true - isUploading = false - - logger.info("上传完成,成功: \(successCount)/\(totalCount)") - } - } -} - -// MARK: - 子视图 - -/// 统计信息视图 -private struct StatView: View { - let value: String - let label: String - let color: Color - - var body: some View { - VStack { - Text(value) - .font(.title3.bold()) - .foregroundColor(color) - Text(label) - .font(.caption) - .foregroundColor(.secondary) - } - .frame(maxWidth: .infinity) - } -} - -// MARK: - 预览 -@available(iOS 16.0, *) -struct MultiImageUploadExampleView_Previews: PreviewProvider { - static var previews: some View { - MultiImageUploadExampleView() - } -} \ No newline at end of file diff --git a/wake/View/Examples/MultiImageUploadExampleView.swift b/wake/View/Examples/MultiImageUploadExampleView.swift new file mode 100644 index 0000000..0409f1f --- /dev/null +++ b/wake/View/Examples/MultiImageUploadExampleView.swift @@ -0,0 +1,162 @@ +import SwiftUI +import PhotosUI + +struct MultiImageUploadExampleView: View { + @State private var isImagePickerPresented = false + @State private var selectedImages: [UIImage]? = [] + @State private var uploadResults: [UploadResult] = [] + @State private var showAlert = false + @State private var alertMessage = "" + + var body: some View { + ScrollView { + VStack(spacing: 20) { + // Custom upload button with image count + MultiImageUploader( + maxSelection: 10, + isImagePickerPresented: $isImagePickerPresented, + selectedImagesBinding: $selectedImages, + content: { isUploading, count in + VStack(spacing: 8) { + Image(systemName: "photo.stack") + .font(.system(size: 24)) + + if isUploading { + ProgressView() + .padding(.vertical, 4) + Text("上传中...") + .font(.subheadline) + } else { + Text(count > 0 ? "已选择 \(count) 张图片" : "选择图片") + .font(.headline) + Text("最多可选择10张图片") + .font(.caption) + .foregroundColor(.secondary) + } + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue.opacity(0.1)) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.blue, lineWidth: 1) + ) + }, + onUploadComplete: handleUploadComplete + ) + .padding(.horizontal) + .padding(.top, 20) + + // Selected images preview with progress + if let images = selectedImages, !images.isEmpty { + VStack(alignment: .leading, spacing: 12) { + Text("已选择图片") + .font(.headline) + .padding(.horizontal) + + LazyVGrid(columns: [ + GridItem(.flexible(), spacing: 8), + GridItem(.flexible(), spacing: 8), + GridItem(.flexible(), spacing: 8) + ], spacing: 8) { + ForEach(Array(images.enumerated()), id: \.offset) { index, image in + ZStack(alignment: .topTrailing) { + Image(uiImage: image) + .resizable() + .scaledToFill() + .frame(height: 100) + .clipped() + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.gray.opacity(0.3), lineWidth: 1) + ) + + // Upload progress indicator + if index < uploadResults.count { + let result = uploadResults[index] + VStack { + Spacer() + ZStack(alignment: .leading) { + Rectangle() + .fill(Color.gray.opacity(0.2)) + .frame(height: 4) + + if case .uploading(let progress) = result.status { + Rectangle() + .fill(Color.blue) + .frame(width: CGFloat(progress) * 100, height: 4) + } else if case .success = result.status { + Rectangle() + .fill(Color.green) + .frame(height: 4) + } else if case .failure = result.status { + Rectangle() + .fill(Color.red) + .frame(height: 4) + } + } + .cornerRadius(2) + .padding(.horizontal, 2) + .padding(.bottom, 2) + } + .frame(height: 20) + } + + // Status indicator + if index < uploadResults.count { + let result = uploadResults[index] + Circle() + .fill(statusColor(for: result.status)) + .frame(width: 12, height: 12) + .padding(4) + .background(Circle().fill(Color.white)) + .padding(4) + } + } + } + } + .padding(.horizontal) + } + } + + Spacer() + } + } + .navigationTitle("多图上传示例") + .alert(isPresented: $showAlert) { + Alert(title: Text("上传结果"), message: Text(alertMessage), dismissButton: .default(Text("确定"))) + } + } + + private func handleUploadComplete(_ results: [UploadResult]) { + self.uploadResults = results + let successCount = results.filter { + if case .success = $0.status { return true } + return false + }.count + + alertMessage = "上传完成!共 \(results.count) 张图片,成功 \(successCount) 张" + showAlert = true + } + + private func statusColor(for status: UploadStatus) -> Color { + switch status { + case .uploading: + return .blue + case .success: + return .green + case .failure: + return .red + default: + return .gray + } + } +} + +#Preview { + NavigationView { + MultiImageUploadExampleView() + } +}