Ios: Perform Upload Task While App Is in Background

How to use session.uploadTask in background i am getting crash

To upload using background URLSessionConfiguration there are a few special considerations:

  1. Cannot use completion handlers (because the app might not be running when you finish the upload). You must use delegate-base methods, e.g. uploadTask(with:fromFile:).

    For example:

     func startUpload(for request: URLRequest, from data: Data) throws -> URLSessionUploadTask {
    let fileURL = URL(fileURLWithPath: NSTemporaryDirectory())
    .appendingPathComponent(UUID().uuidString)
    try data.write(to: fileURL)
    let task = session.uploadTask(with: request, fromFile: fileURL)
    task.resume()

    return task
    }

    That obviously assumes that you’ve specified your delegate and implemented the appropriate delegate methods:

     extension BackgroundSession: URLSessionDelegate {
    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    DispatchQueue.main.async {
    self.savedCompletionHandler?()
    self.savedCompletionHandler = nil
    }
    }
    }

    extension BackgroundSession: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    if let error = error {
    print(error)
    return
    }
    print("success")
    }
    }
  2. Note, we cannot use uploadTask(with:from:) because that’s using Data for that second parameter, which is not allowed for background sessions. Instead one must save the body of the request into a file and then use uploadTask(with:fromFile:).

  3. Remember to handle the scenario where the upload finishes when your app is not running. Namely, the app delegate’s handleEventsForBackgroundURLSession must capture the completion handler. For example, I’ll have a property in my BackgroundSession to save the completion handler:

     func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { 
    BackgroundSession.shared.savedCompletionHandler = completionHandler
    }

    And then you want to implement urlSessionDidFinishEvents(forBackgroundURLSession:) and call the saved completion handler:

     extension BackgroundSession: URLSessionDelegate {
    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    DispatchQueue.main.async {
    self.savedCompletionHandler?()
    self.savedCompletionHandler = nil
    }
    }
    }

By the way, Downloading Files in the Background discusses many of these considerations (e.g. delegate API rather than closure based API, app delegate issues, etc.). It even discusses the requirements that upload tasks are file-based, too.


Anyway, here is a sample BackgroundSession manager:

import os.log

// Note, below I will use `os_log` to log messages because when testing background URLSession
// you do not want to be attached to a debugger (because doing so changes the lifecycle of an
// app). So, I will use `os_log` rather than `print` debugging statements because I can then
// see these logging statements in my macOS `Console` without using Xcode at all. I'll log these
// messages using this `OSLog` so that I can easily filter the macOS `Console` for just these
// logging statements.

private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: #file)

class BackgroundSession: NSObject {
var savedCompletionHandler: (() -> Void)?

static var shared = BackgroundSession()

private var session: URLSession!

private override init() {
super.init()

let identifier = Bundle.main.bundleIdentifier! + ".backgroundSession"
let configuration = URLSessionConfiguration.background(withIdentifier: identifier)

session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
}

extension BackgroundSession {
@discardableResult
func startUpload(for request: URLRequest, from data: Data) throws -> URLSessionUploadTask {
os_log("%{public}@: start", log: log, type: .debug, #function)

let fileURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
try data.write(to: fileURL)
let task = session.uploadTask(with: request, fromFile: fileURL)
task.resume()

return task
}
}

extension BackgroundSession: URLSessionDelegate {
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
os_log(#function, log: log, type: .debug)

DispatchQueue.main.async {
self.savedCompletionHandler?()
self.savedCompletionHandler = nil
}
}
}

extension BackgroundSession: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
os_log("%{public}@: %{public}@", log: log, type: .error, #function, error.localizedDescription)
return
}
os_log("%{public}@: SUCCESS", log: log, type: .debug, #function)
}
}

extension BackgroundSession: URLSessionDataDelegate {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didBecome downloadTask: URLSessionDownloadTask) {
os_log(#function, log: log, type: .debug)
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
os_log("%{public}@: received %d", log: log, type: .debug, #function, data.count)
}
}

And, of course, my app delegate:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

...

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
BackgroundSession.shared.savedCompletionHandler = completionHandler
}
}

Needless to say, that’s a lot to worry about with uploads in conjunction with background URLSession. If you’re uploading huge assets (e.g. videos) over slow connections, perhaps you need that. But the other (simpler) alternative is to use a default URLSession configuration, and just tell the OS that even if the user leaves your app, request a little more time to finish the upload. Just use standard completion handler pattern with default URLSession, and marry that with the techniques outlined in Extending Your App's Background Execution Time. Now, that only buys you 30 seconds or so (it used to be 3 minutes in older iOS versions), but often that’s all we need. But if you think it may take more than 30 seconds to finish the uploads, then you’ll need background URLSessionConfiguration.

using begin​Background​Task​With​Expiration​Handler​ for upload

This background task will let your app continue to run in background after the user leaves your app for an extra 3 minutes or so (check background​Time​Remaining for actual value) to let your request finish. And, yes, near the end of that 3 minutes, the timeout handler will be called if you haven't yet ended the background task.

So, if you end the background task during the normal flow of your app, this timeout closure won't need to be called. This closure is solely for any quick, last minute cleanup, that you might need to do before your app stops running in the background because it timed out before you had a chance to indicate that the background task ended. It's not for starting anything new, but just any last second clean-up. And make sure to end the background task in this timeout handler ... if you don't end the background task, the OS will summarily kill your app rather than just suspending it. Often, the only thing you need to do in this timeout closure is end the background task, but if you need to do any other cleanup, this is where you can do it.

Needless to say, you must end your background task (either when the network request finishes, or in the timeout handler if your app didn't yet get a chance to end the background task in its normal flow). If you don't, your app won't just be suspended, but rather it will be killed.

Regarding making assumptions about what happens when your app is restarted by the user later, you can't make any assumes about which app delegate method will be called. Even if you gracefully ended the background task, you have no assurances that it won't get jettisoned for other reasons (e.g. memory pressure). So don't assume anything.

NSURLSessionDataTask in background session

See WWDC 2014 video What's New in Foundation Networking, about 49 minutes in. The bottom line is that you can perform data tasks in background sessions now, but it will only work only if the app is running. If app is suspended or terminated, it cannot perform data task, though you can convert it to a download task when it receives the response. (FWIW, I don't find it particularly useful to have a background data task that can only work while the app is still running.)

I'm not sure why you're worrying about download vs data task. It strikes me that you could just initiate a download task and then in didFinishDownloadingToURL, look at downloadTask.response.

Having said that I'm unclear what your broader intent is. If you want to ping your server (e.g. to see if data is available for download), you'd generally use background fetch for that.

Background upload of large amount of data

Unfortunately, Apple's APIs are terrible at this task because of bad design decisions. There are a couple of major obstacles that you face:

  • There are limits to how many simultaneous tasks you can create. I think performance starts to break down at somewhere on the order of 100 tasks.
  • There are limits to how often the OS will wake your app up. The more often it wakes your app, the longer it will wait before waking it again. At some point, this will result in not being able to schedule new tasks.
  • Uploads don't continue from where they left off in the event of a failure; they restart. This can result in huge bandwidth costs on a bad network.

I suspect that the best approach is to:

  • Chunk the requests into several large groups, each of which can be written to a single ZIP archive that is not so big that the user runs out of disk space, but not so small that it uploads too quickly.
  • Write the first set of those files into a single file (e.g. in ZIP format).
  • Use a custom script on the server side that lets you resume uploads from where it left off by adding extra CGI parameters.
  • On failure, ask the server how much data it got, then truncate the front of the file and reupload from the current spot.
  • On success, compute how quickly the first large file finished uploading, and if it is not O(minutes), combine the next few sets. Write the set/sets to a file, and start the next request.

With the caveat that all of this must be done fairly quickly. You may find it necessary to pre-combine the files into ZIP archives ahead of time to avoid getting killed. But do not be tempted to combine them into a single file, because then you'll take too long when truncating the head on retry. (Ostensibly, you could also provide any parameters as part of the URL, and make the POST body be raw data, and provide a file stream to read from the ZIP archive starting at an offset.)

If you're not banging your head against a wall already, you soon will be. :-)



Related Topics



Leave a reply



Submit