Article

Downloading Files in the Background

Create tasks that download files while your app is inactive.

Overview

For long-running and nonurgent transfers, you can create tasks that run in the background. These tasks continue to run even when your app is suspended, allowing your app to access the downloaded file when the app is resumed.

Configure the Background Session

Create a background URL session as follows. Listing 1 demonstrates this process.

  1. Create a background URLSessionConfiguration object with the class method background(withIdentifier:) of URLSession, providing a session identifier that is unique within your app. Because most apps need only a few background sessions (usually one), you can use a fixed string for the identifier, rather than a dynamically generated identifier. The identifier doesn’t need to be unique globally.

  2. To have the system to wake up your app when a task completes and the app is in the background, make sure the sessionSendsLaunchEvents property is set to true (the default).

  3. For time-insensitive tasks, enable the isDiscretionary property, so the system can wait for optimal conditions to perform the transfer, such as when the device is plugged in or connected to Wi-Fi.

  4. Use the URLSessionConfiguration instance to create a URLSession instance. The session must provide a delegate, to receive events from the background transfer.

Listing 1

Creating a background URL session

private lazy var urlSession: URLSession = {
    let config = URLSessionConfiguration.background(withIdentifier: "MySession")
    config.isDiscretionary = true
    config.sessionSendsLaunchEvents = true
    return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()

Create and Schedule the Download Task

You create download tasks from the session with either the downloadTask(with:) method that takes a URL, or the downloadTask(with:) method that takes a URLRequest instance. You set properties on this method to help the system optimize its behavior.

  1. As shown in Listing 2, create a download task with downloadTask(with:).

  2. Optionally, set the earliestBeginDate property to schedule the download to begin at some point in the future. The download isn’t guaranteed to begin at precisely this time, but it won’t start sooner.

  3. To help the system efficiently schedule network activity, set the properties countOfBytesClientExpectsToSend and countOfBytesClientExpectsToReceive. These values are best-guess upper bounds on the expected byte count, and should account for headers as well as body data.

  4. To start the task, call resume().

In Listing 2, the task is set to begin at least one hour in the future and is configured to send around 200 bytes of data and receive around 500 KB.

Listing 2

Creating a download task from a URL session

let backgroundTask = urlSession.downloadTask(with: url)
backgroundTask.earliestBeginDate = Date().addingTimeInterval(60 * 60)
backgroundTask.countOfBytesClientExpectsToSend = 200
backgroundTask.countOfBytesClientExpectsToReceive = 500 * 1024
backgroundTask.resume()

Handle App Suspension

Different app states affect how your app interacts with the background download. In iOS, your app could be in the foreground, suspended, or even terminated by the system. See Managing Your App's Life Cycle for more information about these states.

If your app is in the background, the system may suspend your app while the download is performed in another process. In this case, when the download finishes, the system resumes the app and calls the UIApplicationDelegate method application(_:handleEventsForBackgroundURLSession:completionHandler:). This method receives the session identifier you created in Listing 1 as its second parameter.

This delegate method also receives a completion handler as its final parameter. You should immediately store this handler wherever it makes sense for your app, perhaps as a property of your app delegate, or of your class that implements URLSessionDownloadDelegate. In Listing 3, this completion handler is stored in an app delegate property called backgroundCompletionHandler.

Listing 3

Storing the background download completion handler sent to the application delegate

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

When all events have been delivered, the system calls the urlSessionDidFinishEvents(forBackgroundURLSession:) method of URLSessionDelegate. At this point, fetch the backgroundCompletionHandler stored by the app delegate in Listing 3 and execute it. Listing 4 shows this process.

Note that because urlSessionDidFinishEvents(forBackgroundURLSession:) may be called on a secondary queue, it needs to explicitly execute the handler (which was received from a UIKit method) on the main queue.

Listing 4

Executing the background URL session completion handler on the main queue

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    DispatchQueue.main.async {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
            let backgroundCompletionHandler =
            appDelegate.backgroundCompletionHandler else {
                return
        }
        backgroundCompletionHandler()
    }
}

Recreate the Session If the App Was Terminated

If the system terminated the app while it was suspended, the system relaunches the app in the background. As part of your launch time setup, recreate the background session (see Listing 1), using the same session identifier as before, to allow the system to reassociate the background download task with your session. You do this so your background session is ready to go whether the app was launched by the user or by the system. Once the app relaunches, the series of events is the same as if the app had been suspended and resumed, as discussed earlier in Handle App Suspension.

Handle Download Completion and Errors

After the app resumes (or is in the foreground), your implementation of URLSessionDownloadDelegate receives callbacks to update the status of the transfer.

You handle the completion of the download by implementing urlSession(_:downloadTask:didFinishDownloadingTo:). Check the downloadTask response to see if a server-side error code like 404 is indicated; if so, there is no downloaded file to retrieve and you should abort. If the download is successful, the final parameter is a local URL where the file has been stored. This location is valid only until the end of the callback, so you must move it to another location, such as the app’s Documents directory.

Listing 5 shows an implementation of urlSession(_:downloadTask:didFinishDownloadingTo:). The implementation completes the background download of a large file by checking for a server-side error and then moving the file to the Documents directory.

Listing 5

Retrieving the downloaded file

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
                didFinishDownloadingTo location: URL) {
    guard let httpResponse = downloadTask.response as? HTTPURLResponse,
        (200...299).contains(httpResponse.statusCode) else {
            print ("server error")
            return
    }
    do {
        let documentsURL = try
            FileManager.default.url(for: .documentDirectory,
                                    in: .userDomainMask,
                                    appropriateFor: nil,
                                    create: false)
        let savedURL = documentsURL.appendingPathComponent(
            location.lastPathComponent)
        try FileManager.default.moveItem(at: location, to: savedURL)
    } catch {
        print ("file error: \(error)")
    }
}

To handle a client-side download error, like failing to connect to the host, implement urlSession(_:task:didCompleteWithError:). This method is called when any task completes; you only have to handle an error if the final parameter is non-nil.

Comply with Background Transfer Limitations

With background sessions, the actual transfer is performed by a process that is separate from your app’s process. Because restarting your app’s process is fairly expensive, a few features are unavailable, resulting in the following limitations:

  • The session must provide a delegate for event delivery. (For uploads and downloads, the delegates behave the same as for in-process transfers.)

  • Only HTTP and HTTPS protocols are supported (no custom protocols).

  • Redirects are always followed. As a result, even if you have implemented urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:), it is not called.

  • Only upload tasks from a file are supported (uploads from data instances or a stream fail after the app exits).

See Also

First Steps

Fetching Website Data into Memory

Receive data directly into memory by creating a data task from a URL session.

Uploading Data to a Website

Post data from your app to servers.

class URLSession

An object that coordinates a group of related network data transfer tasks.

class URLSessionConfiguration

A configuration object that defines behavior and policies for a URL session.

class URLSessionTask

A task, like downloading a specific resource, performed in a URL session.