我需要从我的 iOS 设备上传大量照片到服务器。例如,500 张照片。我应该如何正确地做到这一点?
我使用 NSURLSession 为每张照片创建了带有后台会话配置的上传任务。我尝试制作自己的自定义队列,其中下一个任务将在上一个任务完成后启动。但有一刻新的任务还没有开始。我想,因为这一切都是在后台发生的。有关此问题的更多信息,您可以阅读此处
(如果使用队列的方法是正确的,请您建议异步任务的队列的良好实现,因为我可能会在实现中弄乱一些东西)
因此,在上面链接的文章之后,我的猜测是我应该立即启动所有上传任务(而不是一个接一个)。但我有效率问题。如果我为每张照片创建一个任务,它将有 500 个后台异步任务和大约 1 GB 的并行数据上传。我想,这会导致网络出现问题。
综上所述,iOS中后台上传大块数据(我的情况是500张照片)的正确方法是什么?
不幸的是,由于糟糕的设计决策,Apple 的 API 在这项任务上表现得很糟糕。 您面临几个主要障碍:
我怀疑最好的方法是:
需要注意的是,所有这些都必须相当快地完成。 您可能会发现有必要提前将文件预先合并到 ZIP 存档中,以避免被杀死。 但不要试图将它们合并到一个文件中,因为这样在重试时截断头部时会花费太长时间。 (表面上,您还可以提供任何参数作为 URL 的一部分,并使 POST 正文成为原始数据,并提供一个文件流以从 ZIP 存档中从偏移量开始读取。)
如果你还没有用头撞墙,那么你很快就会这么做。 :-)
我最近有一个类似的需求,我需要处理大量媒体文件的后台上传(我的例子是 75 个)。我能够使用带有后台配置的 URLSession 成功实现它。关键是正确管理会话、处理进度更新,并确保即使应用程序进入后台也能继续上传。这是我解决问题的方法...
// Helper extension to append Data
extension Data {
mutating func append(_ string: String) {
if let data = string.data(using: .utf8) {
append(data)
}
}
}
class BackgroundUploader: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
var backgroundSession: URLSession!
var uploadCompletionHandlers: [Int: (String) -> Void] = [:] // Store completion handlers for each task
var files = String()
override init() {
super.init()
// Create a background configuration
let config = URLSessionConfiguration.background(withIdentifier: "com.yourapp.upload")
config.isDiscretionary = false // Ensure uploads run even when the app is in the background
config.sessionSendsLaunchEvents = true // App will relaunch if terminated during upload
// Create the background session
backgroundSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self) // Remove observer when this object is deallocated
}
// Function to upload an array of data with headers and completion handler
func uploadDataArray(dataArray: [Data], to url: URL, completionHandler: @escaping (String) -> Void) {
let boundary = "Boundary-\(UUID().uuidString)" // Generate a unique boundary for the request
let multipartBody = createMultipartBody(dataArray: dataArray, boundary: boundary)
// Write multipart body to a temporary file
let tempDirectory = FileManager.default.temporaryDirectory
let tempFileURL = tempDirectory.appendingPathComponent(UUID().uuidString)
do {
// Write the multipart body to a temp file
try multipartBody.write(to: tempFileURL)
// Upload the file
uploadFile(fileURL: tempFileURL, to: url, boundary: boundary, completionHandler: completionHandler)
} catch {
print("Failed to write multipart body to temporary file: \(error)")
}
}
// Create a multipart body with all files from the array
private func createMultipartBody(dataArray: [Data], boundary: String) -> Data {
var body = Data()
for (index, data) in dataArray.enumerated() {
// Add each file to the body as "files[]"
let filename = "file\(index + 1).jpg" // Update the filename for each file
let mimeType = "image/jpeg" // Change MIME type as needed
body.append("--\(boundary)\r\n")
body.append("Content-Disposition: form-data; name=\"files[]\"; filename=\"\(filename)\"\r\n")
body.append("Content-Type: \(mimeType)\r\n\r\n")
body.append(data)
body.append("\r\n")
}
// Final boundary to signal the end of the multipart form
body.append("--\(boundary)--\r\n")
return body
}
// Upload the file using file URL with headers
func uploadFile(fileURL: URL, to url: URL, boundary: String, completionHandler: @escaping (String) -> Void) {
var request = URLRequest(url: url)
request.httpMethod = "POST"
// Set custom headers (multipart/form-data and Authorization headers)
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(AIUser.sharedManager.token)", forHTTPHeaderField: "Authorization")
request.setValue(APP_VERSION ?? "", forHTTPHeaderField: "app-version")
// Create a background upload task for the file
let task = backgroundSession.uploadTask(with: request, fromFile: fileURL)
// Store the completion handler associated with the task ID
uploadCompletionHandlers[task.taskIdentifier] = completionHandler
task.resume()
}
// Handle response and pass uploaded file name to completion handler
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
do {
// Parse the JSON response
if let responseJSON = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
print("Response JSON: \(responseJSON)")
// Check if responseData contains images
if let responseData = responseJSON["responseData"] as? [String: Any],
let images = responseData["images"] as? [String] {
// Combine the image names into a single string (if needed)
let joinedFileNames = images.joined(separator: ", ")
self.files = joinedFileNames
if let completionHandler = uploadCompletionHandlers[dataTask.taskIdentifier] {
completionHandler(joinedFileNames) // Pass the uploaded file names back
}
// Call the associated completion handler with the uploaded file names
} else if let successMessage = responseJSON["message"] as? String {
// Handle the message if images aren't present
if let completionHandler = uploadCompletionHandlers[dataTask.taskIdentifier] {
completionHandler(successMessage) // Pass the response message back
}
}
}
} catch {
print("Failed to parse response: \(error)")
}
}
// URLSessionTaskDelegate: Handle completion in the background
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Upload failed with error: \(error.localizedDescription)")
} else {
print("Upload completed successfully for task: \(task.taskIdentifier)")
scheduleUploadCompletionNotification()
}
// Remove the stored handler for the completed task
uploadCompletionHandlers.removeValue(forKey: task.taskIdentifier)
}
// Optional: Handle app relaunch after task completion in background
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
// Notify the system that all background events have been handled
print("All background events have been handled.")
}
@objc func appWillEnterForeground() {
// Check if there are any active tasks
backgroundSession.getAllTasks { tasks in
if tasks.isEmpty {
// No active tasks, call your API from the controller
DispatchQueue.main.async {
// Assume you have a reference to your controller, call the API here
print("Entered in forground and uploaded files are \(self.files)")
}
} else {
print("There are still \(tasks.count) uploads in progress.")
}
}
}
func scheduleUploadCompletionNotification() {
let content = UNMutableNotificationContent()
content.title = "Upload Complete"
content.body = "All your files have been uploaded successfully."
content.sound = .default
// Set a trigger to show the notification immediately
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
// Create the notification request
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
// Add the notification request to the system
let center = UNUserNotificationCenter.current()
center.add(request) { error in
if let error = error {
print("Error scheduling notification: \(error)")
} else {
print("Notification scheduled!")
}
}
}
}
use:-
var uploader : BackgroundUploader!
uploader.uploadDataArray(dataArray: [Data], to: URL, completionHandler: (String) -> Void)