feat: 添加中文注释
This commit is contained in:
parent
ef65019e46
commit
d15acc9038
Binary file not shown.
@ -5,22 +5,15 @@ import PhotosUI
|
|||||||
|
|
||||||
/// 照片选择器,封装了系统相册选择功能
|
/// 照片选择器,封装了系统相册选择功能
|
||||||
struct PhotoPicker: UIViewControllerRepresentable {
|
struct PhotoPicker: UIViewControllerRepresentable {
|
||||||
// MARK: - Properties
|
|
||||||
|
|
||||||
@Binding var selectedImages: [UIImage]
|
@Binding var selectedImages: [UIImage]
|
||||||
|
|
||||||
|
/// 最多可选图片数量
|
||||||
let selectionLimit: Int
|
let selectionLimit: Int
|
||||||
|
|
||||||
|
/// 图片过滤器,默认为图片类型
|
||||||
let filter: PHPickerFilter
|
let filter: PHPickerFilter
|
||||||
var onImageUploaded: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
|
|
||||||
var onUploadProgress: ((ImageUploadService.UploadProgress) -> Void)?
|
|
||||||
@Environment(\.presentationMode) private var presentationMode
|
|
||||||
|
|
||||||
// MARK: - Initialization
|
init(selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images) {
|
||||||
|
|
||||||
init(selectedImages: Binding<[UIImage]>,
|
|
||||||
selectionLimit: Int = 1,
|
|
||||||
filter: PHPickerFilter = .images,
|
|
||||||
onImageUploaded: ((Result<ImageUploadService.UploadResults, Error>) -> Void)? = nil,
|
|
||||||
onUploadProgress: ((ImageUploadService.UploadProgress) -> Void)? = nil) {
|
|
||||||
self._selectedImages = selectedImages
|
self._selectedImages = selectedImages
|
||||||
self.selectionLimit = selectionLimit
|
self.selectionLimit = selectionLimit
|
||||||
self.filter = filter
|
self.filter = filter
|
||||||
@ -30,7 +23,13 @@ struct PhotoPicker: UIViewControllerRepresentable {
|
|||||||
|
|
||||||
// MARK: - UIViewControllerRepresentable
|
// MARK: - UIViewControllerRepresentable
|
||||||
|
|
||||||
|
// MARK: - UIViewControllerRepresentable 协议方法
|
||||||
|
|
||||||
|
/// 创建并返回PHPickerViewController实例
|
||||||
|
/// - Parameter context: 上下文
|
||||||
|
/// - Returns: 配置好的PHPickerViewController
|
||||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||||
|
// 配置照片选择器
|
||||||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||||||
configuration.filter = filter
|
configuration.filter = filter
|
||||||
configuration.selectionLimit = selectionLimit
|
configuration.selectionLimit = selectionLimit
|
||||||
@ -41,139 +40,109 @@ struct PhotoPicker: UIViewControllerRepresentable {
|
|||||||
return picker
|
return picker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 更新视图控制器
|
||||||
|
/// - Parameters:
|
||||||
|
/// - uiViewController: 要更新的视图控制器
|
||||||
|
/// - context: 上下文
|
||||||
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
||||||
|
|
||||||
|
/// 创建协调器
|
||||||
|
/// - Returns: 协调器实例
|
||||||
func makeCoordinator() -> Coordinator {
|
func makeCoordinator() -> Coordinator {
|
||||||
Coordinator(self)
|
Coordinator(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Coordinator
|
// MARK: - Coordinator
|
||||||
|
|
||||||
|
// MARK: - 协调器类
|
||||||
|
|
||||||
|
/// 协调器,处理PHPickerViewController的代理方法
|
||||||
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||||||
|
/// 父视图引用
|
||||||
let parent: PhotoPicker
|
let parent: PhotoPicker
|
||||||
private let uploadService = ImageUploadService.shared
|
private let uploadService = ImageUploadService.shared
|
||||||
|
|
||||||
|
/// 初始化方法
|
||||||
|
/// - Parameter parent: 父视图
|
||||||
init(_ parent: PhotoPicker) {
|
init(_ parent: PhotoPicker) {
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 用户完成图片选择时调用
|
||||||
|
/// - Parameters:
|
||||||
|
/// - picker: 图片选择器
|
||||||
|
/// - results: 选中的图片结果
|
||||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||||
guard !results.isEmpty else {
|
|
||||||
parent.presentationMode.wrappedValue.dismiss()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.selectedImages.removeAll()
|
parent.selectedImages.removeAll()
|
||||||
|
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
var loadedImages: [Int: UIImage] = [:]
|
var loadedImages: [Int: UIImage] = [:]
|
||||||
var uploadResults: [Int: ImageUploadService.UploadResults] = [:]
|
var uploadResults: [Int: ImageUploadService.UploadResults] = [:]
|
||||||
var lastError: Error?
|
var lastError: Error?
|
||||||
|
|
||||||
|
// 遍历所有选中的图片
|
||||||
for (index, result) in results.enumerated() {
|
for (index, result) in results.enumerated() {
|
||||||
group.enter()
|
group.enter()
|
||||||
|
|
||||||
|
// 检查是否支持加载图片
|
||||||
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
|
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
|
||||||
|
// 异步加载图片
|
||||||
result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
|
result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
|
||||||
if let error = error {
|
if let image = image as? UIImage {
|
||||||
lastError = error
|
loadedImages[index] = image
|
||||||
group.leave()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
group.leave()
|
||||||
guard let image = image as? UIImage else {
|
|
||||||
lastError = NSError(domain: "com.wake.upload", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to load image"])
|
|
||||||
group.leave()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedImages[index] = image
|
|
||||||
|
|
||||||
// Upload the image
|
|
||||||
self.uploadService.uploadOriginalAndCompressedImage(
|
|
||||||
image,
|
|
||||||
compressionQuality: 0.5,
|
|
||||||
progress: { progress in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.parent.onUploadProgress?(progress)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
completion: { result in
|
|
||||||
defer { group.leave() }
|
|
||||||
|
|
||||||
switch result {
|
|
||||||
case .success(let results):
|
|
||||||
uploadResults[index] = results
|
|
||||||
|
|
||||||
// Upload file info to backend
|
|
||||||
MaterialService.shared.uploadMaterialInfo(
|
|
||||||
fileId: results.original.fileId,
|
|
||||||
previewFileId: results.compressed.fileId
|
|
||||||
) { success, errorMessage in
|
|
||||||
if success {
|
|
||||||
print("✅ 文件信息上传成功")
|
|
||||||
} else if let errorMessage = errorMessage {
|
|
||||||
print("❌ 文件信息上传失败: \(errorMessage)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case .failure(let error):
|
|
||||||
lastError = error
|
|
||||||
print("❌ 图片上传失败: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
group.leave()
|
group.leave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
group.notify(queue: .main) { [weak self] in
|
group.notify(queue: .main) {
|
||||||
guard let self = self else { return }
|
// 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)
|
||||||
|
|
||||||
if let error = lastError {
|
// Dismiss the picker
|
||||||
self.parent.onImageUploaded?(.failure(error))
|
picker.dismiss(animated: true)
|
||||||
} else {
|
|
||||||
let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value }
|
|
||||||
self.parent.selectedImages.append(contentsOf: sortedImages)
|
|
||||||
|
|
||||||
if let firstResult = uploadResults.first?.value {
|
|
||||||
self.parent.onImageUploaded?(.success(firstResult))
|
|
||||||
} else {
|
|
||||||
self.parent.onImageUploaded?(.failure(NSError(
|
|
||||||
domain: "com.wake.upload",
|
|
||||||
code: -1,
|
|
||||||
userInfo: [NSLocalizedDescriptionKey: "上传过程中出现错误"]
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.parent.presentationMode.wrappedValue.dismiss()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Avatar Uploader
|
// MARK: - Avatar Uploader Component
|
||||||
|
|
||||||
/// 头像上传视图,提供头像选择功能
|
|
||||||
struct AvatarUploader: View {
|
struct AvatarUploader: View {
|
||||||
|
// MARK: - 属性
|
||||||
|
|
||||||
|
/// 当前选中的头像图片
|
||||||
@Binding var selectedImage: UIImage?
|
@Binding var selectedImage: UIImage?
|
||||||
|
|
||||||
|
/// 头像尺寸
|
||||||
let size: CGFloat
|
let size: CGFloat
|
||||||
var onUploadComplete: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
|
var onUploadComplete: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
|
||||||
|
|
||||||
|
// MARK: - 状态
|
||||||
|
|
||||||
|
/// 是否显示图片选择器
|
||||||
@State private var isImagePickerPresented = false
|
@State private var isImagePickerPresented = false
|
||||||
|
|
||||||
|
// MARK: - 视图
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: { isImagePickerPresented = true }) {
|
Button(action: {
|
||||||
|
isImagePickerPresented = true
|
||||||
|
}) {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
// Avatar Image or Placeholder
|
||||||
if let selectedImage = selectedImage {
|
if let selectedImage = selectedImage {
|
||||||
|
// 已选择的头像图片
|
||||||
Image(uiImage: selectedImage)
|
Image(uiImage: selectedImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: size * 0.1))
|
.clipShape(RoundedRectangle(cornerRadius: size * 0.1))
|
||||||
} else {
|
} else {
|
||||||
|
// Default avatar container
|
||||||
Color.gray.opacity(0.1)
|
Color.gray.opacity(0.1)
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.overlay(
|
.overlay(
|
||||||
@ -188,24 +157,19 @@ struct AvatarUploader: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle()) // Make the entire area tappable
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle()) // Remove button highlight effect
|
||||||
.sheet(isPresented: $isImagePickerPresented) {
|
.sheet(isPresented: $isImagePickerPresented) {
|
||||||
PhotoPicker(
|
PhotoPicker(
|
||||||
|
// 绑定选中的图片
|
||||||
selectedImages: Binding(
|
selectedImages: Binding(
|
||||||
get: { [selectedImage].compactMap { $0 } },
|
get: { [selectedImage].compactMap { $0 } },
|
||||||
set: { images in
|
set: { images in
|
||||||
selectedImage = images.first
|
selectedImage = images.first
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
selectionLimit: 1,
|
selectionLimit: 1
|
||||||
onImageUploaded: { result in
|
|
||||||
onUploadComplete?(result)
|
|
||||||
},
|
|
||||||
onUploadProgress: { progress in
|
|
||||||
print("上传进度:\(progress.current)/\(progress.total),进度:\(Int(progress.progress * 100))%")
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,354 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import PhotosUI
|
||||||
|
|
||||||
|
/// 上传响应数据结构体,用于解析服务器返回的上传URL和表单字段
|
||||||
|
struct UploadResponse: Codable {
|
||||||
|
let url: String // 上传文件的目标URL
|
||||||
|
let fields: [String: String] // 上传所需的表单字段
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 图片上传管理器,负责处理图片上传相关逻辑
|
||||||
|
class ImageUploader: ObservableObject {
|
||||||
|
// MARK: - 发布属性
|
||||||
|
@Published var isUploading = false // 是否正在上传
|
||||||
|
@Published var uploadProgress: Double = 0 // 上传进度
|
||||||
|
@Published var error: Error? // 上传错误信息
|
||||||
|
|
||||||
|
// 基础API地址
|
||||||
|
private let baseURL = "https://api.memorywake.com/api/v1"
|
||||||
|
|
||||||
|
/// 上传图片到服务器
|
||||||
|
/// - Parameters:
|
||||||
|
/// - image: 要上传的图片
|
||||||
|
/// - completion: 完成回调,返回上传结果
|
||||||
|
func uploadImage(_ image: UIImage, completion: @escaping (Result<String, Error>) -> Void) {
|
||||||
|
// 1. 将图片转换为JPEG数据
|
||||||
|
guard let imageData = image.jpegData(compressionQuality: 0.8) else {
|
||||||
|
completion(.failure(NSError(domain: "", code: -1,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: "图片数据转换失败"])))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构建获取上传URL的请求
|
||||||
|
guard let url = URL(string: "\(baseURL)/iam/file/generate-upload-url") else {
|
||||||
|
completion(.failure(NSError(domain: "", code: -1,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: "无效的URL"])))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
// 3. 发起获取上传URL的请求
|
||||||
|
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
// 处理网络错误
|
||||||
|
if let error = error {
|
||||||
|
DispatchQueue.main.async { completion(.failure(error)) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查响应数据
|
||||||
|
guard let data = data else {
|
||||||
|
let error = NSError(domain: "", code: -1,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: "未收到服务器响应"])
|
||||||
|
DispatchQueue.main.async { completion(.failure(error)) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
// 4. 解析上传URL响应
|
||||||
|
let uploadResponse = try JSONDecoder().decode(UploadResponse.self, from: data)
|
||||||
|
// 5. 开始实际上传文件
|
||||||
|
self.uploadFile(to: uploadResponse.url,
|
||||||
|
with: uploadResponse.fields,
|
||||||
|
imageData: imageData,
|
||||||
|
completion: completion)
|
||||||
|
} catch {
|
||||||
|
DispatchQueue.main.async { completion(.failure(error)) }
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 实际上传文件到指定的URL
|
||||||
|
/// - Parameters:
|
||||||
|
/// - url: 上传URL
|
||||||
|
/// - fields: 上传所需的表单字段
|
||||||
|
/// - imageData: 图片数据
|
||||||
|
/// - completion: 完成回调
|
||||||
|
private func uploadFile(to url: String,
|
||||||
|
with fields: [String: String],
|
||||||
|
imageData: Data,
|
||||||
|
completion: @escaping (Result<String, Error>) -> Void) {
|
||||||
|
|
||||||
|
// 验证上传URL
|
||||||
|
guard let uploadURL = URL(string: url) else {
|
||||||
|
completion(.failure(NSError(domain: "", code: -1,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: "无效的上传URL"])))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置上传请求
|
||||||
|
var request = URLRequest(url: uploadURL)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
|
||||||
|
// 设置multipart/form-data边界
|
||||||
|
let boundary = "Boundary-\(UUID().uuidString)"
|
||||||
|
request.setValue("multipart/form-data; boundary=\(boundary)",
|
||||||
|
forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
// 构建请求体
|
||||||
|
var body = Data()
|
||||||
|
|
||||||
|
// 添加表单字段
|
||||||
|
for (key, value) in fields {
|
||||||
|
body.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||||||
|
body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
|
||||||
|
body.append("\(value)\r\n".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加图片数据
|
||||||
|
let filename = "\(UUID().uuidString).jpg"
|
||||||
|
body.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||||||
|
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
|
||||||
|
body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
|
||||||
|
body.append(imageData)
|
||||||
|
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
|
||||||
|
|
||||||
|
request.httpBody = body
|
||||||
|
|
||||||
|
// 执行上传任务
|
||||||
|
URLSession.shared.dataTask(with: request) { _, _, error in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let error = error {
|
||||||
|
completion(.failure(error))
|
||||||
|
} else {
|
||||||
|
completion(.success("上传成功"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 照片选择器,封装了系统相册选择功能
|
||||||
|
struct PhotoPicker: UIViewControllerRepresentable {
|
||||||
|
// MARK: - 属性
|
||||||
|
@Binding var selectedImages: [UIImage] // 绑定的已选图片数组
|
||||||
|
let selectionLimit: Int // 最多可选图片数量
|
||||||
|
let filter: PHPickerFilter // 图片过滤器
|
||||||
|
var onUploadComplete: ((Result<String, Error>) -> Void)? = nil // 上传完成回调
|
||||||
|
private let uploader = ImageUploader() // 图片上传器
|
||||||
|
|
||||||
|
// MARK: - 初始化方法
|
||||||
|
init(selectedImages: Binding<[UIImage]>,
|
||||||
|
selectionLimit: Int = 1,
|
||||||
|
filter: PHPickerFilter = .images,
|
||||||
|
onUploadComplete: ((Result<String, Error>) -> Void)? = nil) {
|
||||||
|
self._selectedImages = selectedImages
|
||||||
|
self.selectionLimit = selectionLimit
|
||||||
|
self.filter = filter
|
||||||
|
self.onUploadComplete = onUploadComplete
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIViewControllerRepresentable 协议方法
|
||||||
|
|
||||||
|
/// 创建并返回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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新视图控制器
|
||||||
|
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
||||||
|
|
||||||
|
/// 创建协调器
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 协调器类
|
||||||
|
|
||||||
|
/// 协调器,处理PHPickerViewController的代理方法
|
||||||
|
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||||||
|
let parent: PhotoPicker
|
||||||
|
|
||||||
|
init(_ parent: PhotoPicker) {
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户完成图片选择时调用
|
||||||
|
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||||
|
parent.selectedImages.removeAll()
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
var loadedImages: [Int: UIImage] = [:]
|
||||||
|
|
||||||
|
// 加载选中的图片
|
||||||
|
for (index, result) in results.enumerated() {
|
||||||
|
group.enter()
|
||||||
|
|
||||||
|
// 检查是否支持加载图片
|
||||||
|
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
|
||||||
|
result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in
|
||||||
|
if let image = image as? UIImage {
|
||||||
|
// 保存加载的图片
|
||||||
|
loadedImages[index] = image
|
||||||
|
|
||||||
|
// 立即上传图片
|
||||||
|
self?.parent.uploader.uploadImage(image) { result in
|
||||||
|
self?.parent.onUploadComplete?(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有图片加载完成后的处理
|
||||||
|
group.notify(queue: .main) {
|
||||||
|
// 按原始顺序排序图片
|
||||||
|
let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value }
|
||||||
|
self.parent.selectedImages.append(contentsOf: sortedImages)
|
||||||
|
|
||||||
|
// 关闭选择器
|
||||||
|
picker.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 头像上传组件
|
||||||
|
|
||||||
|
/// 头像上传视图,提供头像选择、上传和显示功能
|
||||||
|
struct AvatarUploader: View {
|
||||||
|
// MARK: - 属性
|
||||||
|
@Binding var selectedImage: UIImage? // 当前选中的头像图片
|
||||||
|
let size: CGFloat // 头像尺寸
|
||||||
|
|
||||||
|
// MARK: - 状态
|
||||||
|
@State private var isImagePickerPresented = false // 是否显示图片选择器
|
||||||
|
@State private var isUploading = false // 是否正在上传
|
||||||
|
@State private var uploadError: Error? // 上传错误信息
|
||||||
|
|
||||||
|
// MARK: - 视图
|
||||||
|
var body: some View {
|
||||||
|
Button(action: showImagePicker) {
|
||||||
|
ZStack {
|
||||||
|
// 显示选中的图片或默认占位图
|
||||||
|
if let selectedImage = selectedImage {
|
||||||
|
// 已选中的头像图片
|
||||||
|
Image(uiImage: selectedImage)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: size * 0.1))
|
||||||
|
} else {
|
||||||
|
// 默认头像占位图
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传加载指示器
|
||||||
|
if isUploading {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
.scaleEffect(1.5)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(Color.black.opacity(0.5))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: size * 0.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.contentShape(Rectangle()) // 使整个区域可点击
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle()) // 移除按钮默认样式
|
||||||
|
.alert("上传失败", isPresented: .constant(uploadError != nil)) {
|
||||||
|
Button("确定") {
|
||||||
|
uploadError = nil
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
if let error = uploadError {
|
||||||
|
Text(error.localizedDescription)
|
||||||
|
} else {
|
||||||
|
Text("请重试")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $isImagePickerPresented) {
|
||||||
|
// 图片选择器
|
||||||
|
PhotoPicker(
|
||||||
|
selectedImages: Binding(
|
||||||
|
get: { [selectedImage].compactMap { $0 } },
|
||||||
|
set: { images in
|
||||||
|
selectedImage = images.first
|
||||||
|
}
|
||||||
|
),
|
||||||
|
selectionLimit: 1,
|
||||||
|
onUploadComplete: handleUploadResult
|
||||||
|
)
|
||||||
|
.onAppear {
|
||||||
|
isUploading = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 私有方法
|
||||||
|
|
||||||
|
/// 显示图片选择器
|
||||||
|
private func showImagePicker() {
|
||||||
|
isImagePickerPresented = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理上传结果
|
||||||
|
private func handleUploadResult(_ result: Result<String, Error>) {
|
||||||
|
isUploading = false
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let message):
|
||||||
|
print("上传成功: \(message)")
|
||||||
|
case .failure(let error):
|
||||||
|
uploadError = error
|
||||||
|
print("上传失败: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 预览提供程序
|
||||||
|
#if DEBUG
|
||||||
|
struct AvatarUploader_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
// 默认状态预览
|
||||||
|
AvatarUploader(selectedImage: .constant(nil), size: 100)
|
||||||
|
.padding()
|
||||||
|
.previewDisplayName("默认状态")
|
||||||
|
|
||||||
|
// 已选择图片状态预览
|
||||||
|
AvatarUploader(
|
||||||
|
selectedImage: .constant(UIImage(systemName: "person.crop.circle.fill")),
|
||||||
|
size: 120
|
||||||
|
)
|
||||||
|
.padding()
|
||||||
|
.previewDisplayName("已选择图片")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Loading…
x
Reference in New Issue
Block a user