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) -> 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) -> 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) -> Void)? = nil // 上传完成回调 private let uploader = ImageUploader() // 图片上传器 // MARK: - 初始化方法 init(selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images, onUploadComplete: ((Result) -> 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) { 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