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 PhotosUI
|
||||
import os.log
|
||||
import os
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
public struct MultiImageUploader: View {
|
||||
@State private var selectedImages: [UIImage] = []
|
||||
public struct MultiImageUploader<Content: View>: 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<Bool>,
|
||||
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<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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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