feat: 多选图片
This commit is contained in:
parent
89112e36e6
commit
1fca9f413c
BIN
wake/CoreData/.DS_Store
vendored
Normal file
BIN
wake/CoreData/.DS_Store
vendored
Normal file
Binary file not shown.
58
wake/View/Components/Upload/ImagePicker.swift
Normal file
58
wake/View/Components/Upload/ImagePicker.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,258 +1,175 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
import os.log
|
import os
|
||||||
|
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
public struct MultiImageUploader: View {
|
public struct MultiImageUploader<Content: View>: View {
|
||||||
@State private var selectedImages: [UIImage] = []
|
@State var selectedImages: [UIImage] = []
|
||||||
@State private var uploadResults: [UploadResult] = []
|
@State private var uploadResults: [UploadResult] = []
|
||||||
@State private var isUploading = false
|
@State private var isUploading = false
|
||||||
@State private var showingImagePicker = false
|
@State private var showingImagePicker = false
|
||||||
@State private var uploadProgress: [UUID: Double] = [:] // 跟踪每个上传任务的进度
|
@State private var uploadProgress: [UUID: Double] = [:] // 跟踪每个上传任务的进度
|
||||||
|
@State private var needsViewUpdate = false // Add this line to trigger view updates
|
||||||
|
|
||||||
private let maxSelection: Int
|
private let maxSelection: Int
|
||||||
private let onUploadComplete: ([UploadResult]) -> Void
|
public var onUploadComplete: ([UploadResult]) -> Void
|
||||||
private let uploadService = ImageUploadService.shared
|
private let uploadService = ImageUploadService.shared
|
||||||
private let logger = Logger(subsystem: "com.yourapp.uploader", category: "MultiImageUploader")
|
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(
|
public init(
|
||||||
maxSelection: Int = 10,
|
maxSelection: Int = 10,
|
||||||
|
isImagePickerPresented: Binding<Bool>,
|
||||||
|
selectedImagesBinding: Binding<[UIImage]?>,
|
||||||
|
@ViewBuilder content: @escaping (_ isUploading: Bool, _ selectedCount: Int) -> Content,
|
||||||
onUploadComplete: @escaping ([UploadResult]) -> Void
|
onUploadComplete: @escaping ([UploadResult]) -> Void
|
||||||
) {
|
) {
|
||||||
self.maxSelection = maxSelection
|
self.maxSelection = maxSelection
|
||||||
|
self._isImagePickerPresented = isImagePickerPresented
|
||||||
|
self._selectedImagesBinding = selectedImagesBinding
|
||||||
|
self.content = content
|
||||||
|
self.onUploadComplete = onUploadComplete
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化方法 - 使用默认按钮样式(向后兼容)
|
||||||
|
public init(
|
||||||
|
maxSelection: Int = 10,
|
||||||
|
isImagePickerPresented: Binding<Bool>,
|
||||||
|
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
|
self.onUploadComplete = onUploadComplete
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
// 上传按钮
|
// 自定义内容或默认按钮
|
||||||
Button(action: {
|
if let content = content {
|
||||||
showingImagePicker = true
|
Button(action: {
|
||||||
}) {
|
showingImagePicker = true
|
||||||
Label("选择图片", systemImage: "photo.on.rectangle")
|
}) {
|
||||||
|
content(isUploading, selectedImages.count)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
} else {
|
||||||
|
// 默认按钮样式
|
||||||
|
Button(action: {
|
||||||
|
showingImagePicker = true
|
||||||
|
}) {
|
||||||
|
Label(
|
||||||
|
!selectedImages.isEmpty ?
|
||||||
|
"已选择 \(selectedImages.count) 张图片" :
|
||||||
|
"选择图片",
|
||||||
|
systemImage: "photo.on.rectangle"
|
||||||
|
)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding()
|
.padding()
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.background(Color.blue)
|
.background(Color.blue)
|
||||||
.cornerRadius(10)
|
.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(.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
|
isUploading = true
|
||||||
let group = DispatchGroup()
|
uploadResults = selectedImages.map {
|
||||||
var results = selectedImages.map { image in
|
UploadResult(image: $0, status: .uploading(progress: 0))
|
||||||
UploadResult(fileId: "", previewFileId: "", image: image, status: .uploading(progress: 0))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新UI显示上传中的状态
|
let group = DispatchGroup()
|
||||||
uploadResults = results
|
|
||||||
|
|
||||||
// 创建并发的DispatchQueue
|
|
||||||
let uploadQueue = DispatchQueue(label: "com.wake.uploadQueue", attributes: .concurrent)
|
|
||||||
let semaphore = DispatchSemaphore(value: 3) // 限制并发数为3
|
|
||||||
|
|
||||||
for (index, image) in selectedImages.enumerated() {
|
for (index, image) in selectedImages.enumerated() {
|
||||||
semaphore.wait()
|
|
||||||
group.enter()
|
group.enter()
|
||||||
|
|
||||||
uploadQueue.async {
|
// 使用 ImageUploadService 上传图片
|
||||||
let resultId = results[index].id
|
uploadService.uploadOriginalAndCompressedImage(
|
||||||
|
image,
|
||||||
// 更新状态为上传中
|
compressionQuality: 0.7,
|
||||||
DispatchQueue.main.async {
|
progress: { progress in
|
||||||
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 {
|
DispatchQueue.main.async {
|
||||||
uploadProgress[resultId] = progress.progress
|
if index < self.uploadResults.count {
|
||||||
if let idx = results.firstIndex(where: { $0.id == resultId }) {
|
self.uploadResults[index].status = .uploading(progress: progress.progress)
|
||||||
results[idx].status = .uploading(progress: progress.progress)
|
self.needsViewUpdate.toggle() // Trigger view update
|
||||||
uploadResults = results
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) { uploadResult in
|
},
|
||||||
defer {
|
completion: { result in
|
||||||
semaphore.signal()
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let idx = results.firstIndex(where: { $0.id == resultId }) {
|
guard index < self.uploadResults.count else { return }
|
||||||
switch uploadResult {
|
switch result {
|
||||||
case .success(let uploadResult):
|
case .success(let uploadResults):
|
||||||
// 上传成功,更新结果
|
self.uploadResults[index].status = .success
|
||||||
results[idx].fileId = uploadResult.fileId
|
self.uploadResults[index].fileId = uploadResults.original.fileId
|
||||||
// 使用空字符串作为 previewFileId,因为 ImageUploaderGetID.UploadResult 没有这个属性
|
self.uploadResults[index].previewFileId = uploadResults.compressed.fileId
|
||||||
results[idx].previewFileId = ""
|
self.needsViewUpdate.toggle() // Trigger view update
|
||||||
results[idx].status = .success
|
case .failure(let error):
|
||||||
uploadResults = results
|
self.uploadResults[index].status = .failure(error)
|
||||||
|
self.needsViewUpdate.toggle() // Trigger view update
|
||||||
logger.info("图片上传成功: \(uploadResult.fileId)")
|
self.logger.error("图片上传失败: \(error.localizedDescription)")
|
||||||
|
|
||||||
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()
|
group.leave()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
group.leave()
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await withCheckedContinuation { continuation in
|
||||||
group.notify(queue: .main) {
|
group.notify(queue: .main) {
|
||||||
self.parent.images = newImages
|
self.isUploading = false
|
||||||
self.parent.onDismiss?()
|
self.needsViewUpdate.toggle() // Trigger view update
|
||||||
|
continuation.resume(returning: self.uploadResults)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
162
wake/View/Examples/MultiImageUploadExampleView.swift
Normal file
162
wake/View/Examples/MultiImageUploadExampleView.swift
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user