feat: 图片上传互获取url

This commit is contained in:
jinyaqiu 2025-08-19 16:32:26 +08:00
parent 45a39fecb9
commit b6391f0734
3 changed files with 166 additions and 395 deletions

View File

@ -1,6 +1,163 @@
import SwiftUI
import PhotosUI
///
///
class ImageUploader: ObservableObject {
private let baseURL = "https://api.memorywake.com/api/v1/file/generate-upload-url"
func uploadImage(_ image: UIImage, completion: @escaping (Result<String, Error>) -> Void) {
print("🔄 开始准备上传图片...")
// 1. Data
guard let imageData = image.jpegData(compressionQuality: 0.7) else {
let error = NSError(domain: "ImageError", code: -1, userInfo: [NSLocalizedDescriptionKey: "图片数据转换失败"])
print("❌ 错误:\(error.localizedDescription)")
completion(.failure(error))
return
}
// 2. URL
guard let url = URL(string: baseURL) else {
let error = NSError(domain: "URLError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的URL"])
print("❌ 错误:\(error.localizedDescription)")
completion(.failure(error))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJqdGkiOjczNjM0ODY2MTE1MDc2NDY0NjQsImlkZW50aXR5IjoiNzM1MDQzOTY2MzExNjYxOTg4OCIsImV4cCI6MTc1NjE5NjgxNX0.hRC_So6LHuR6Gx-bDyO8aliVOd-Xumul8M7cydi2pTxHPweBx4421AfZ5BjGoEEwRZPIXJ5z7a1aDB7qvjpLCA", forHTTPHeaderField: "Authorization")
// 3.
let fileName = "avatar_\(UUID().uuidString).jpg"
let parameters: [String: Any] = [
"filename": fileName,
"content_type": "image/jpeg",
"file_size": imageData.count
]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
print("📤 准备上传请求,文件名: \(fileName), 大小: \(Double(imageData.count) / 1024.0) KB")
print("📡 请求参数: \(parameters)")
} catch {
print("❌ 序列化请求参数失败: \(error.localizedDescription)")
completion(.failure(error))
return
}
// 4.
print("🌐 请求URL: \(url.absoluteString)")
print("📋 请求头: \(request.allHTTPHeaderFields ?? [:])")
// 5. URL
print("🌐 正在获取上传链接...")
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
// 线
DispatchQueue.main.async {
if let error = error {
print("❌ 请求失败: \(error.localizedDescription)")
completion(.failure(error))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
let error = NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的服务器响应"])
print("❌ 错误:\(error.localizedDescription)")
completion(.failure(error))
return
}
print("📊 响应状态码: \(httpResponse.statusCode)")
print("📦 响应头: \(httpResponse.allHeaderFields)")
guard let data = data else {
let error = NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "没有接收到数据"])
print("❌ 错误:\(error.localizedDescription)")
completion(.failure(error))
return
}
//
if let responseString = String(data: data, encoding: .utf8) {
print("📡 原始响应数据: \(responseString)")
}
// 5. URL
do {
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw NSError(domain: "ParseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "响应数据格式错误"])
}
print("📋 解析后的JSON: \(json)")
// API
if let uploadUrlString = json["url"] as? String,
let uploadUrl = URL(string: uploadUrlString) {
print("✅ 成功获取上传链接: \(uploadUrlString)")
self?.uploadImageData(imageData, to: uploadUrl, fileName: fileName, completion: completion)
} else {
throw NSError(domain: "APIError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法获取上传链接: \(json)"])
}
} catch {
print("❌ 解析响应数据失败: \(error.localizedDescription)")
completion(.failure(error))
}
}
}
task.resume()
}
private func uploadImageData(_ data: Data, to url: URL, fileName: String, completion: @escaping (Result<String, Error>) -> Void) {
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("image/jpeg", forHTTPHeaderField: "Content-Type")
request.httpBody = data
print("🚀 开始上传图片数据...")
let task = URLSession.shared.dataTask(with: request) { _, response, error in
DispatchQueue.main.async {
if let error = error {
print("❌ 上传失败: \(error.localizedDescription)")
completion(.failure(error))
return
}
//
if let httpResponse = response as? HTTPURLResponse {
print("📊 上传响应状态码: \(httpResponse.statusCode)")
if (200...299).contains(httpResponse.statusCode) {
let fileUrl = "https://your-cdn-domain.com/\(fileName)" // CDN
print("✅ 上传成功文件URL: \(fileUrl)")
completion(.success(fileUrl))
} else {
let errorMessage = "上传失败,状态码: \(httpResponse.statusCode)"
print("\(errorMessage)")
let error = NSError(domain: "UploadError",
code: httpResponse.statusCode,
userInfo: [NSLocalizedDescriptionKey: errorMessage])
completion(.failure(error))
}
} else {
let errorMessage = "无效的服务器响应"
print("\(errorMessage)")
let error = NSError(domain: "UploadError",
code: -1,
userInfo: [NSLocalizedDescriptionKey: errorMessage])
completion(.failure(error))
}
}
}
task.resume()
}
}
///
struct PhotoPicker: UIViewControllerRepresentable {
// MARK: -
@ -14,13 +171,12 @@ struct PhotoPicker: UIViewControllerRepresentable {
///
let filter: PHPickerFilter
///
private let uploader = ImageUploader()
// MARK: -
///
/// - Parameters:
/// - selectedImages:
/// - selectionLimit: 1
/// - filter: .images
init(selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images) {
self._selectedImages = selectedImages
self.selectionLimit = selectionLimit
@ -29,11 +185,7 @@ struct PhotoPicker: 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
@ -44,54 +196,41 @@ struct PhotoPicker: UIViewControllerRepresentable {
return picker
}
///
/// - Parameters:
/// - uiViewController:
/// - context:
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
///
/// - Returns:
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// MARK: -
/// PHPickerViewController
class Coordinator: NSObject, PHPickerViewControllerDelegate {
///
let parent: PhotoPicker
///
/// - Parameter parent:
init(_ parent: PhotoPicker) {
self.parent = parent
}
///
/// - Parameters:
/// - picker:
/// - results:
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
//
parent.selectedImages.removeAll()
// 使DispatchGroup
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) { (image, error) in
if let image = image as? UIImage {
//
loadedImages[index] = image
//
self.parent.uploader.uploadImage(image) { result in
//
//
}
}
group.leave()
}
@ -100,14 +239,9 @@ struct PhotoPicker: UIViewControllerRepresentable {
}
}
//
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)
}
}
@ -135,20 +269,16 @@ struct AvatarUploader: View {
var body: some View {
Button(action: {
//
isImagePickerPresented = true
}) {
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(
@ -163,22 +293,17 @@ struct AvatarUploader: View {
}
}
.frame(width: size, height: size)
// 使
.contentShape(Rectangle())
}
//
.buttonStyle(PlainButtonStyle())
//
.sheet(isPresented: $isImagePickerPresented) {
PhotoPicker(
//
selectedImages: Binding(
get: { [selectedImage].compactMap { $0 } },
set: { images in
selectedImage = images.first
}
),
//
selectionLimit: 1
)
}

View File

@ -1,354 +0,0 @@
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