feat: 添加中文注释

This commit is contained in:
jinyaqiu 2025-08-19 15:43:15 +08:00
parent ef65019e46
commit d15acc9038
3 changed files with 415 additions and 97 deletions

View File

@ -5,22 +5,15 @@ import PhotosUI
///
struct PhotoPicker: UIViewControllerRepresentable {
// MARK: - Properties
@Binding var selectedImages: [UIImage]
///
let selectionLimit: Int
///
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,
onImageUploaded: ((Result<ImageUploadService.UploadResults, Error>) -> Void)? = nil,
onUploadProgress: ((ImageUploadService.UploadProgress) -> Void)? = nil) {
init(selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images) {
self._selectedImages = selectedImages
self.selectionLimit = selectionLimit
self.filter = filter
@ -30,7 +23,13 @@ struct PhotoPicker: UIViewControllerRepresentable {
// 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
@ -41,139 +40,109 @@ struct PhotoPicker: UIViewControllerRepresentable {
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]) {
guard !results.isEmpty else {
parent.presentationMode.wrappedValue.dismiss()
return
}
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 error = error {
lastError = error
group.leave()
return
if let image = image as? UIImage {
loadedImages[index] = image
}
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)")
}
}
)
group.leave()
}
} else {
group.leave()
}
}
group.notify(queue: .main) { [weak self] in
guard let self = self else { return }
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)
if let error = lastError {
self.parent.onImageUploaded?(.failure(error))
} 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()
// Dismiss the picker
picker.dismiss(animated: true)
}
}
}
}
// MARK: - Avatar Uploader
///
// 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 }) {
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(
@ -188,24 +157,19 @@ struct AvatarUploader: View {
}
}
.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) {
PhotoPicker(
//
selectedImages: Binding(
get: { [selectedImage].compactMap { $0 } },
set: { images in
selectedImage = images.first
}
),
selectionLimit: 1,
onImageUploaded: { result in
onUploadComplete?(result)
},
onUploadProgress: { progress in
print("上传进度:\(progress.current)/\(progress.total),进度:\(Int(progress.progress * 100))%")
}
selectionLimit: 1
)
}
}

View File

@ -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