176 lines
6.2 KiB
Swift
176 lines
6.2 KiB
Swift
import SwiftUI
|
||
import PhotosUI
|
||
|
||
// MARK: - Photo Picker
|
||
|
||
/// 照片选择器,封装了系统相册选择功能
|
||
struct PhotoPicker: UIViewControllerRepresentable {
|
||
@Binding var selectedImages: [UIImage]
|
||
|
||
/// 最多可选图片数量
|
||
let selectionLimit: Int
|
||
|
||
/// 图片过滤器,默认为图片类型
|
||
let filter: PHPickerFilter
|
||
|
||
init(selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images) {
|
||
self._selectedImages = selectedImages
|
||
self.selectionLimit = selectionLimit
|
||
self.filter = filter
|
||
self.onImageUploaded = onImageUploaded
|
||
self.onUploadProgress = onUploadProgress
|
||
}
|
||
|
||
// MARK: - UIViewControllerRepresentable
|
||
|
||
// MARK: - UIViewControllerRepresentable 协议方法
|
||
|
||
/// 创建并返回PHPickerViewController实例
|
||
/// - Parameter context: 上下文
|
||
/// - Returns: 配置好的PHPickerViewController
|
||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||
// 配置照片选择器
|
||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||
configuration.filter = filter
|
||
configuration.selectionLimit = selectionLimit
|
||
configuration.preferredAssetRepresentationMode = .current
|
||
|
||
let picker = PHPickerViewController(configuration: configuration)
|
||
picker.delegate = context.coordinator
|
||
return picker
|
||
}
|
||
|
||
/// 更新视图控制器
|
||
/// - Parameters:
|
||
/// - uiViewController: 要更新的视图控制器
|
||
/// - context: 上下文
|
||
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
||
|
||
/// 创建协调器
|
||
/// - Returns: 协调器实例
|
||
func makeCoordinator() -> Coordinator {
|
||
Coordinator(self)
|
||
}
|
||
|
||
// MARK: - Coordinator
|
||
|
||
// MARK: - 协调器类
|
||
|
||
/// 协调器,处理PHPickerViewController的代理方法
|
||
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||
/// 父视图引用
|
||
let parent: PhotoPicker
|
||
private let uploadService = ImageUploadService.shared
|
||
|
||
/// 初始化方法
|
||
/// - Parameter parent: 父视图
|
||
init(_ parent: PhotoPicker) {
|
||
self.parent = parent
|
||
}
|
||
|
||
/// 用户完成图片选择时调用
|
||
/// - Parameters:
|
||
/// - picker: 图片选择器
|
||
/// - results: 选中的图片结果
|
||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||
parent.selectedImages.removeAll()
|
||
|
||
let group = DispatchGroup()
|
||
var loadedImages: [Int: UIImage] = [:]
|
||
var uploadResults: [Int: ImageUploadService.UploadResults] = [:]
|
||
var lastError: Error?
|
||
|
||
// 遍历所有选中的图片
|
||
for (index, result) in results.enumerated() {
|
||
group.enter()
|
||
|
||
// 检查是否支持加载图片
|
||
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
|
||
// 异步加载图片
|
||
result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
|
||
if let image = image as? UIImage {
|
||
loadedImages[index] = image
|
||
}
|
||
group.leave()
|
||
}
|
||
} else {
|
||
group.leave()
|
||
}
|
||
}
|
||
|
||
group.notify(queue: .main) {
|
||
// Sort the images by their original index to maintain selection order
|
||
let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value }
|
||
self.parent.selectedImages.append(contentsOf: sortedImages)
|
||
|
||
// Dismiss the picker
|
||
picker.dismiss(animated: true)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Avatar Uploader Component
|
||
struct AvatarUploader: View {
|
||
// MARK: - 属性
|
||
|
||
/// 当前选中的头像图片
|
||
@Binding var selectedImage: UIImage?
|
||
|
||
/// 头像尺寸
|
||
let size: CGFloat
|
||
var onUploadComplete: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
|
||
|
||
// MARK: - 状态
|
||
|
||
/// 是否显示图片选择器
|
||
@State private var isImagePickerPresented = false
|
||
|
||
// MARK: - 视图
|
||
|
||
var body: some View {
|
||
Button(action: {
|
||
isImagePickerPresented = true
|
||
}) {
|
||
ZStack {
|
||
// Avatar Image or Placeholder
|
||
if let selectedImage = selectedImage {
|
||
// 已选择的头像图片
|
||
Image(uiImage: selectedImage)
|
||
.resizable()
|
||
.scaledToFill()
|
||
.frame(width: size, height: size)
|
||
.clipShape(RoundedRectangle(cornerRadius: size * 0.1))
|
||
} else {
|
||
// Default avatar container
|
||
Color.gray.opacity(0.1)
|
||
.frame(width: size, height: size)
|
||
.overlay(
|
||
SVGImage(svgName: "Avatar")
|
||
.frame(width: size * 0.8, height: size * 0.8)
|
||
)
|
||
.clipShape(RoundedRectangle(cornerRadius: size * 0.1))
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: size * 0.1)
|
||
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
|
||
)
|
||
}
|
||
}
|
||
.frame(width: size, height: size)
|
||
.contentShape(Rectangle()) // Make the entire area tappable
|
||
}
|
||
.buttonStyle(PlainButtonStyle()) // Remove button highlight effect
|
||
.sheet(isPresented: $isImagePickerPresented) {
|
||
PhotoPicker(
|
||
// 绑定选中的图片
|
||
selectedImages: Binding(
|
||
get: { [selectedImage].compactMap { $0 } },
|
||
set: { images in
|
||
selectedImage = images.first
|
||
}
|
||
),
|
||
selectionLimit: 1
|
||
)
|
||
}
|
||
}
|
||
} |