Downloading files in background with URLSessionDownloadTask

This snippet demonstrates how to use URLSessionDownloadTask to download files in background so that they can completed even if the app is terminated. It also shows how to implement progress monitoring for multiple tasks running in parallel:

Downloads with progress monitoring

Starting downloads

To start a download that can be completed in background, even if the app is terminated, create a URLSessionConfiguration for background processing. The identifier will identify the URLSession: if the process is terminated and later restarted, you can get the “same” URLSession f.e. to ask about the progress of downloads in progress:

let config = URLSessionConfiguration.background(withIdentifier: "com.example.DownloadTaskExample.background")

Then create a URLSession object. This can be observed using a URLSessionTaskDelegate:

let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())

But, beware: If an URLSession still exists from a previous download in the same process, it doesn’t create a new URLSession object but returns the existing one with the old delegate object attached! It will give you a warning about this behaviour “A background URLSession with identifier … already exists!”:

Then create a URLSessionDownloadTask:

let url = URL(string: "https://example.com/example.pdf")!
let task = session.downloadTask(with: url)
task.resume()

Observing started downloads

Completion handler blocks are not supported in background sessions - the URLSession delegate has to be used for observing the download. When the download is completed, it is written to the Caches directory of the application and the URLSessionDownloadDelegate is notified:

class SomeClass : NSObject, URLSessionTaskDelegate, URLSessionDownloadDelegate {
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        if totalBytesExpectedToWrite > 0 {
            let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
            debugPrint("Progress \(downloadTask) \(progress)")
        }
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        debugPrint("Download finished: \(location)")
        try? FileManager.default.removeItem(at: location)
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        debugPrint("Task completed: \(task), error: \(error)")
    }

}

Because of the impossibility to change the delegate of the URLSession once it was created and to observe the progress of downloads from older processes, it pays off to create an application wide object to keep track of the URLSession object and its delegate:

private class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {

    static var shared = DownloadManager()

    var session : URLSession {
        get {
            let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")

            // Warning: If an URLSession still exists from a previous download, it doesn't create
            // a new URLSession object but returns the existing one with the old delegate object attached!
            return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
        }
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        if totalBytesExpectedToWrite > 0 {
            let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
            debugPrint("Progress \(downloadTask) \(progress)")
        }
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        debugPrint("Download finished: \(location)")
        try? FileManager.default.removeItem(at: location)
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        debugPrint("Task completed: \(task), error: \(error)")
    }

}

If you stop the application while a download is still in progress and run it again, by accessing URLSession with the same identifier, you can get notified about the progress of the downloads and retrieve a notification for all background tasks that have been completed in background. But be aware: the system cancels all background tasks when an application is terminated by swiping up from the app switcher:

Swiping in the application switcher cancels all background tasks

Additionally, if a URL session finishes its work when your app is not running, the system launches your app in the background and calls the AppDelegate method handleEventsForBackgroundURLSession so that it can process those event immediately:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    // ...

    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        debugPrint("handleEventsForBackgroundURLSession: \(identifier)")
        completionHandler()
    }

}

This can be tricky to debug, but it’s possible to see this happening using Debug > Attach to Process by PID or Name...:

Debug process in background

Progress monitoring for multiple tasks

To display the progress of all running tasks, use the delegate method urlSession:downloadTask:didWriteData:. If multiple tasks are running in parallel, you can use URLSession#getTasksWithCompletionHandler to compute the total of all running tasks:

func calculateProgress(session : URLSession, completionHandler : @escaping (Float) -> ()) {
    session.getTasksWithCompletionHandler { (tasks, uploads, downloads) in
        let bytesReceived = downloads.map{ $0.countOfBytesReceived }.reduce(0, +)
        let bytesExpectedToReceive = downloads.map{ $0.countOfBytesExpectedToReceive }.reduce(0, +)
        let progress = bytesExpectedToReceive > 0 ? Float(bytesReceived) / Float(bytesExpectedToReceive) : 0.0
        completionHandler(progress)
    }
}

Example code

More information

Btn training bbbdf557d2 Next iOS training: 10. - 14. September 2018, München
Btn read 3c0e607615 Read on: iOS developer blog
Btn subscribe 930758687e Subscribe: Email · Twitter
Btn share 3139847d21 Share: Email · Twitter
Btn support 789320554c Support the iOS developer blog - Become a patron
Btn about 5378472193 About me
Btn email 4d2439fc5b Email to Ralf Ebert «info@ralfebert.de»