> ## Content Index
> Fetch the complete content index at: https://williamboles.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Keeping Downloads Going with Background Transfers
- URL: https://williamboles.com/keeping-things-going-when-the-user-leaves-with-urlsession-and-background-transfers/
- Published: 2018-07-15T08:29:32.000Z
- Updated: 2026-09-26T16:34:26.000Z
- Description: In iOS, we can use background-transfers to extend the life of our downloads and uploads beyond the app's lifespan, offering our users a better experience by using a background URLSession. Here, we look at how to build that better experience.
- Author: William Boles
- Tags: Networking, Concurrency, The Cat API

> The example in this post is built using GCD to control access to shared mutable state. If you would prefer to use Swift Concurrency, I would recommend that you instead read: ["Keep Downloading with a Background Session"](https://williamboles.com/keep-downloading-with-a-background-session/).

In the past, iOS focused solely on what the user was doing, ensuring a responsive user experience on performance-constrained devices. While beneficial for users, this focus meant that as soon as an app went into the background, iOS suspended it. However, with more powerful and energy-efficient devices, iOS has changed. iOS now allows apps to perform limited background work while maintaining foreground responsiveness.

![Photo of a user leaving](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2019/11/walking-away-min-1.jpg)

It is now possible for download and upload requests to continue when an app has been suspended or even terminated. These network requests are collectively known as `background-transfers`. Support for background-transfers was introduced in iOS 7\. While not a recent change, it remains a powerful tool.

In this post, we'll explore background-transfers and build a working solution to use this functionality.

> This post will gradually build up to a working example, but I get it, this is exciting stuff 😎, so if you are unable to contain that excitement, then head on over to the [completed example](https://github.com/wibosco/BackgroundTransfer-Example?ref=williamboles.com) and take a look at `BackgroundDownloadService` and `BackgroundDownloadStore` to see how things end up.

## Different types of sessions

When Apple introduced the [URLSession](https://developer.apple.com/documentation/foundation/urlsession?ref=williamboles.com) suite of classes, they addressed a number of the shortfalls that were present in a [NSURLConnection](https://developer.apple.com/documentation/foundation/nsurlconnection?ref=williamboles.com) based networking stack. One particular pain point was that with an `NSURLConnection` networking stack, there was no built-in way to group related types of requests together - each request in a group had to be individually configured. `URLSession` changed this by moving that group configuration to a `session`. A session represents a configurable object that handles the configuration and coordination of network requests. An app can have multiple sessions. Any request scheduled on that session would inherit that session's configuration. While useful for app developers to group requests, this session-level configuration also allowed iOS to offer more functionality as standard, enabling support for background-transfers on special *background* sessions.

Each session can be described as one of the following three flavours:

1. **Default** \- supports all [URLSessionTask](https://developer.apple.com/documentation/foundation/urlsessiontask?ref=williamboles.com) subtypes for making a network request. These network requests can only be performed with the app in the foreground.
2. **Ephemeral** \- similar to `default` but does not persist caches, cookies, or credentials to disk.
3. **Background** \- supports all `URLSessionTask` subtypes for making a network request. [URLSessionDataTask](https://developer.apple.com/documentation/foundation/urlsessiondatatask?ref=williamboles.com) can only be performed with the app in the foreground, whereas [URLSessionDownloadTask](https://developer.apple.com/documentation/foundation/urlsessiondownloadtask?ref=williamboles.com) and [URLSessionUploadTask](https://developer.apple.com/documentation/foundation/urlsessionuploadtask?ref=williamboles.com) can be performed with the app in the foreground, suspended or terminated.

Each session has its use case; let's explore how we can use a background session to enable our uploads and downloads to continue when the user leaves the app.

## So, how do background-transfers work? 🕵️

When scheduling a transfer on a background session, that transfer is passed to the `nsurlsessiond` [daemon](https://en.wikipedia.org/wiki/Daemon%5F%28computing%29?ref=williamboles.com) to be processed. As `nsurlsessiond` lives outside of the lifecycle of any given app, any transfer scheduled on `nsurlsessiond` will continue even if the app that the transfer belongs to is suspended/terminated. Once a background-transfer is complete, if the app has been suspended/terminated, iOS will *wake* the app in the background and allow the app to perform any post-transfer processing (within a limited time frame); if the app is in the foreground, control will be passed back to the app as if the transfer has been scheduled on a `default` or `ephemeral` session (without the limited time frame for post-transfer processing).

You might be thinking:

**"That sounds pretty amazing! Why isn't this the default behaviour for transfers?"**

Well, there are a few reasons why this isn't the default behaviour:

- **Resource Management** \- excessive background processing can drain battery life and consume unexpected bandwidth; Apple wants to ensure that, as app developers, we use it responsibly and only where it's actually adding value.
- **Programming Complexity** \- implementing background-transfers requires foregoing the convenient closure-based methods of `URLSession`. Instead, it requires conforming to `URLSessionDownloadDelegate` to receive updates about the transfers, adding complexity when compared to foreground-only sessions.

## Setting up the project

The app must be granted additional permissions to enable background-transfers:

1. Open the `Signing & Capabilities` tab in the target.
2. Add the `Background Modes` capability.
3. In the newly added `Background Modes` section, select the `Background fetch` and `Background processing` checkboxes.

After completing these steps, your settings should look like this:

![Screenshot of background modes](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2025/04/Screenshot-of-configuring-the-project-min.jpg)

With the project correctly configured, it is time to start coding.

## Let's get downloading

Our background-transfer layer has five primary responsibilities:

1. Configuring a background session.
2. Scheduling the download request.
3. Responding to download updates.
4. Processing any completed downloads by moving files to a permanent location on the file system or reporting an error.
5. Recovering from a terminated/suspended state to process completed downloads.

And needs to be able to handle a download completing with the app in the following states:

1. **Foregrounded** \- when the app is open, the app needs to behave as if the session used the `default` configuration.
2. **Suspended** \- when the app has been suspended, the app needs to be woken up to finish the transfer.
3. **Terminated** \- when iOS has terminated the app, the app needs to be relaunched to finish the transfer.

Together, these responsibilities and states produce the following class structure and interactions:

![Class diagram showing how BackgroundDownloadService, BackgroundDownloadStore, AppDelegate and URLSession interact](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2025/03/background-transfer-class-diagram-3.jpg)

Each type has the following role:

- `BackgroundDownloadService` sets up the background session, schedules downloads, and responds to download updates.
- `BackgroundDownloadStore` manages the metadata for each download, ensuring data persistence beyond app launches.
- `AppDelegate` informs `BackgroundDownloadService` when the app has been woken/relaunched to complete background downloads.
- `URLSession` informs `BackgroundDownloadService` of any updates to in-progress downloads via the `URLSessionDownloadDelegate`.

From a high level, the steps involved in making a download are:

> Some steps are marked as optional because they are either invoked or skipped depending on if the app is in the foreground or background.

1. A download is requested on `BackgroundDownloadService`.
2. The requested download's metadata is stored in `BackgroundDownloadStore`.
3. The download is kicked off.
4. The download completes.  
a. **Optional**: If the app is in the suspended/terminated state, it is woken up and `AppDelegate` passes a closure callback to `BackgroundDownloadService` to be called once all downloads are completed.  
b.`BackgroundDownloadService` is informed of the outcome of the download via `URLSessionDownloadDelegate`.
5. Stored metadata is retrieved from `BackgroundDownloadStore` for the download.
6. The downloaded content is moved to its permanent local file system location, and interested parties are informed of the outcome of the download.  
a. **Optional**: Trigger metadata closure callback, if it exists.  
b. **Optional**: Trigger `AppDelegate` closure callback once all downloads have been processed.

> Don't worry if some parts of the above description about roles or the steps don't entirely make sense; each part will be explored in more depth as the solution is built up.

Three pieces of metadata are needed to perform a download:

1. **Remote URL** \- the URL of the content to be downloaded.
2. **File path URL** \- the URL of where the downloaded content should be stored on the local file system.
3. **Completion handler** \- the closure to be called when the download is complete.

Let's take these three pieces of metadata and start implementing `BackgroundDownloadStore`, starting with adding the functionality to store that metadata:

```
typealias BackgroundDownloadCompletion = (_ result: Result<URL, Error>) -> ()

class BackgroundDownloadStore {
    //1
    private var inMemoryStore = [String: BackgroundDownloadCompletion]()

    // 2
    private let persistentStore = UserDefaults.standard

    // MARK: - Store

    // 3
    func storeMetadata(from fromURL: URL,
                       to toURL: URL,
                       completionHandler: @escaping BackgroundDownloadCompletion) {
        inMemoryStore[fromURL.absoluteString] = completionHandler
        persistentStore.set(toURL, forKey: fromURL.absoluteString)
    }
}
```

Here's what we did:

1. Background session downloads don't support the [completion-handler](https://developer.apple.com/documentation/foundation/urlsession/1411511-downloadtask?ref=williamboles.com) methods on `URLSession`; instead, any download progress updates are provided via `URLSessionDownloadDelegate`. However, a closure-based interface is perfect for downloading content where the caller is really only interested in either having the downloaded content or an error. So, to recreate that convenient closure-based interface, `BackgroundDownloadService` takes a closure when a download is requested and passes it onto `BackgroundDownloadStore` to store it in the `inMemoryStore` dictionary. As the outcome of a download can either succeed or fail, the [Result](https://developer.apple.com/documentation/swift/result?ref=williamboles.com) type can represent both outcomes in one type. As the closure here is a communication path between the caller and callee, when the app is terminated, the memory holding the caller and callee will be freed, so the storage of these closures is only in-memory.
2. As a download may complete while the app is terminated, details of where the downloaded content should end up on the local file system must be persisted outside of memory. [UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults?ref=williamboles.com) is a simple and effective means of persisting that information.
3. The method to store a download's metadata into `inMemoryStore` and `persistentStore` using `fromURL` as the key.

Storing metadata is all good, but that metadata needs to be retrieved:

```
class BackgroundDownloadStore {
    // Omitted methods and properties

    // MARK: - Retrieve

    // 1
    func retrieveMetadata(for forURL: URL) -> (URL?, BackgroundDownloadCompletion?) {
        let key = forURL.absoluteString

        let toURL = persistentStore.url(forKey: key)
        let metaDataCompletionHandler = inMemoryStore[key]

        return (toURL, metaDataCompletionHandler)
    }
}
```

Here's what we did:

1. Any metadata associated with `forURL` is extracted from `persistentStore` and `inMemoryStore`. As there is no guarantee that the metadata associated with `forURL` was ever stored (or in the case of the closures stored in `inMemoryStore`, there is no guarantee the closure is still in memory), this method returns that metadata as a tuple with two optional values.

Finally, once a download has been completed, we need a way to remove that download's metadata from the store:

```
class BackgroundDownloadStore {
    // Omitted methods and properties

    // MARK: - Remove

    // 1
    func removeMetadata(for forURL: URL) {
        let key = forURL.absoluteString

        inMemoryStore[key] = nil
        persistentStore.removeObject(forKey: key)
    }
}
```

Here's what we did:

1. Any metadata associated with `forURL` is deleted from `persistentStore` and `inMemoryStore`.

That's all we need to do to store the metadata associated with a download; let's move on to configuring a background session and making a background-transfer.

Before looking at `BackgroundDownloadService`, let's take a short detour and look at a supporting type that will be used in `BackgroundDownloadService`:

```
enum BackgroundDownloadServiceError: Error {
    case missingInstructionsError
    case fileSystemError(_ underlyingError: Error)
    case networkError(_ underlyingError: Error?)
    case unexpectedResponseError
    case unexpectedStatusCode
}
```

`BackgroundDownloadServiceError` is an enum that conforms to the `Error` protocol and will be used to provide details about the unhappy download paths through `BackgroundDownloadService`.

Detour over 🗺️.

Let's build the skeleton of `BackgroundDownloadService`:

```
class BackgroundDownloadService: NSObject { // 1
    // 2
    private var session: URLSession!

    // MARK: - Singleton

    // 3
    static let shared = BackgroundDownloadService()

    // 4
    override init() {
        super.init()

        configureSession()
    }

    private func configureSession() {
        // 5
        let configuration = URLSessionConfiguration.background(withIdentifier: "com.williamboles.background.download.session")

        // 6
        configuration.sessionSendsLaunchEvents = true
        let session = URLSession(configuration: configuration,
                                 delegate: self,
                                 delegateQueue: nil)
        self.session = session
    }
}
```

Let's look at what we did above:

1. As mentioned earlier, background session downloads don't support the [completion-handler](https://developer.apple.com/documentation/foundation/urlsession/1411511-downloadtask?ref=williamboles.com) methods on `URLSession`; Instead `BackgroundDownloadService` will need to become the delegate to the background `URLSession` session by implementing the `URLSessionDownloadDelegate`. The need to conform to `URLSessionDownloadDelegate` is why `BackgroundDownloadService` is a subclass of `NSObject`.
2. There exists a [cyclic dependency](https://en.wikipedia.org/wiki/Circular%5Fdependency?ref=williamboles.com) between `BackgroundDownloadService` and `URLSession`, where `BackgroundDownloadService` is responsible for not only creating the `URLSession` instance but also being its `delegate`. However, the `delegate` of `URLSession` can only be set at initialisation of the `URLSession`. Both these conditions cannot be satisfied. To break the cyclic dependency between `BackgroundDownloadService` and `URLSession`, the `session` property is set after `BackgroundDownloadService` is initialised, and to avoid having an optional property, `session` is implicitly unwrapped.
3. `BackgroundDownloadService` is a singleton to ensure that any background-transfer updates are funnelled into the same type that holds the metadata of that transfer.
4. `init()` is private to ensure that another instance of `BackgroundDownloadService` cannot be created.
5. A session's configuration is defined through a [URLSessionConfiguration](https://developer.apple.com/documentation/foundation/urlsessionconfiguration?ref=williamboles.com) instance, which offers numerous configuration options. Every `URLSessionConfiguration` for a background session requires a unique identifier within that app's lifecycle. If an existing `URLSession` already has that identifier, iOS returns the previously created `URLSession` instance. The session identifier plays a vital role in ensuring that a download can survive app termination - for any download update, iOS will attempt to find a `URLSession` instance with the same identifier that the download was kicked off on to inform of the update; that found `URLSession` instance does not need to be the same instance that was used to kick off the download. To ensure the app always has a `URLSession` that can pick up any download updates, `session` is consistently configured with the same identifier.
6. `sessionSendsLaunchEvents` tells iOS that any background-transfers scheduled on this session can launch the app if it is suspended/terminated.

So far, we have configured a session for performing background-transfers, but can't yet download anything; let's change that in:

```
class BackgroundDownloadService: NSObject {
    // Omitted other properties

    // 1
    private let store = BackgroundDownloadStore()

    // Omitted other methods

    // MARK: - Download

    func download(from remoteURL: URL,
                  saveDownloadTo localURL: URL,
                  completionHandler: @escaping ((_ result: Result<URL, BackgroundDownloadServiceError>) -> ())) {
        os_log(.info, "Scheduling to download: %{public}@", remoteURL.absoluteString)

        // 2
        store.storeMetadata(from: remoteURL,
                            to: localURL,
                            completionHandler: completionHandler)

        // 3
        let task = session.downloadTask(with: remoteURL)
        task.resume()
    }
}
```

Here's what we did:

1. A `BackgroundDownloadStore` instance is stored as a property to ensure access to a download's metadata.
2. The metadata associated with the current download request is added to the store.
3. A `URLSessionDownloadTask` instance is created using the background session, and the download request is kicked off.

> To better see the downloads happening in the background, you may wish to add a slight delay before the download starts, making it easier to put the app into the background: 
> 
> ```
> task.earliestBeginDate = Date().addingTimeInterval(5)
> ```

Now that `BackgroundDownloadService` can kick off a download, we need to add in the code to handle the happy-path when a download completes by implementing `urlSession(_:downloadTask:didFinishDownloadingTo)` from `URLSessionDownloadDelegate`:

```
extension BackgroundDownloadService: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession,
                    downloadTask: URLSessionDownloadTask,
                    didFinishDownloadingTo location: URL) {
        // 1
        guard let fromURL = downloadTask.originalRequest?.url else {
            os_log(.error, "Unexpected nil URL")
            // Unable to call the closure here as fromURL is the key to retrieving this download's closure
            return
        }

        // 2
        defer {
            store.removeMetadata(for: fromURL)
        }

        let fromURLAsString = fromURL.absoluteString

        os_log(.info, "Download request completed for: %{public}@", fromURLAsString)

        // 3
        let (toURL, completionHandler) = store.retrieveMetadata(for: fromURL)

        // 4
        guard let toURL else {
            os_log(.error, "Unable to find existing download item for: %{public}@", fromURLAsString)
            completionHandler?(.failure(BackgroundDownloadError.missingInstructionsError))
            return
        }

        os_log(.info, "Download successful for: %{public}@", fromURLAsString)

        // 5
        do {
            try FileManager.default.moveItem(at: location,
                                             to: toURL)
            // 6
            completionHandler?(.success(toURL))
        } catch {
            completionHandler?(.failure(BackgroundDownloadError.fileSystemError(error)))
        }
    }
}
```

This delegate method can be called when the app is **foregrounded**, **suspended**, or **terminated**, so all three states need to be handled in the same method:

1. In the `download` method, the `remoteURL` was used as the key for storing a download's metadata because that value is present when a download completes in the `URLSessionDownloadTask` instance. A `URLSessionDownloadTask` will automatically follow redirect instructions, so it is essential to ensure that the correct URL is used to connect the completed download with its metadata - specifically, the original URL stored in the `originalRequest` property. If `originalRequest` is nil, the method exits as no further action can be taken.
2. As there are multiple paths through `urlSession(_:downloadTask:didFinishDownloadingTo)`, a `defer` block is used to clean up the download by deleting its metadata on any exit.
3. The downloads' associated metadata is retrieved from the store.
4. If `toURL` is nil, the download cannot be processed because there is no known location to move the downloaded content to. As a result, the method exits with no further action taken.
5. When `URLSessionDownloadTask` completes a download, it will store that downloaded content in a [temporary location](https://developer.apple.com/documentation/foundation/urlsession/1411511-downloadtask?ref=williamboles.com) and allow us to move that downloaded file to a permanent location before iOS deletes it. Using [FileManager](https://developer.apple.com/documentation/foundation/filemanager?ref=williamboles.com), the downloaded content is moved from its temporary location to its permanent location. If the move is successful, the completion handler is called with the file's new location; if the move fails, the completion handler is called with an error.
6. If the app has been relaunched from a terminated state, any closure associated with this download is no longer available, so there is no enforcement that `completionHandler` must be non-nil.

That's the happy path completed; let's add in the unhappy-path.

The unhappy-path comes in two forms: server-side and client-side errors. Let's start with the server-side errors by updating `urlSession(_:downloadTask:didFinishDownloadingTo:)`:

```
extension BackgroundDownloadService: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession,
                    downloadTask: URLSessionDownloadTask,
                    didFinishDownloadingTo location: URL) {
        guard let fromURL = downloadTask.originalRequest?.url else {
            os_log(.error, "Unexpected nil URL")
            // Unable to call the closure here as fromURL is the key to retrieving this download's closure
            return
        }

        defer {
            store.removeMetadata(for: fromURL)
        }

        let fromURLAsString = fromURL.absoluteString

        os_log(.info, "Download request completed for: %{public}@", fromURLAsString)

        let (toURL, completionHandler) = store.retrieveMetadata(for: fromURL)

        guard let toURL else {
            os_log(.error, "Unable to find existing download item for: %{public}@", fromURLAsString)
            completionHandler?(.failure(BackgroundDownloadError.missingInstructionsError))
            return
        }

        // 1
        guard let response = downloadTask.response as? HTTPURLResponse,
                     response.statusCode == 200 else {
             os_log(.error, "Unexpected response for: %{public}@", fromURLAsString)
             completionHandler?(.failure(BackgroundDownloadError.serverError(downloadTask.response)))
             return
         }

        os_log(.info, "Download successful for: %{public}@", fromURLAsString)

        do {
            try FileManager.default.moveItem(at: location,
                                             to: toURL)

            completionHandler?(.success(toURL))
        } catch {
            completionHandler?(.failure(BackgroundDownloadError.fileSystemError(error)))
        }
    }
}
```

Here's what we changed:

1. To determine if a download was successful, a **[200](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/200?ref=williamboles.com)** HTTP status code is expected. Here, the response is checked. If the status code is anything other than a 200, then `completionHandler` is triggered with `serverError`.

> If your server returns other status codes that should be treated as success, then it is simple enough to add those by refactoring the `guard` to accommodate those additional status codes.

Now that server-side errors are handled, let's take care of any client-side errors:

```
extension BackgroundDownloadService: URLSessionDownloadDelegate {
    // Omitted other methods

    func urlSession(_ session: URLSession,
                    task: URLSessionTask,
                    didCompleteWithError error: Error?) {
        // 1
        guard let error = error else {
            return
        }

        // 2
        guard let fromURL = task.originalRequest?.url else {
            os_log(.error, "Unexpected nil URL")
            return
        }

        let fromURLAaString = fromURL.absoluteString

        os_log(.info, "Download failed for: %{public}@", fromURLAaString)

        // 3
        let (_, completionHandler) = store.retrieveMetadata(for: fromURL)

        completionHandler?(.failure(BackgroundDownloadError.clientError(error)))

        // 4
        store.removeMetadata(for: fromURL)
    }
}
```

Here's what we did:

1. This delegate method is called when any network request completes, meaning it will be triggered for both successful and unsuccessful network requests. If a network request fails, `error` will contain a value. To exclude successful downloads from the unhappy-path, `error` is checked for `nil`. If `error` is `nil`, the method exits.
2. Like in `urlSession(_:downloadTask:didFinishDownloadingTo:)`, the remote URL is extracted from the completed `URLSessionTask` instance as `fromURL`. If `originalRequest` is `nil`, the method exits as no further action can be taken.
3. The closure associated with this download is retrieved from `store` and triggered with `networkError`.
4. The now-complete downloads metadata is deleted.

If you've been putting together the code snippets, you should be able to download files in the foreground; however, as soon as the app is put into the background, a stream of errors will begin to appear in the Xcode console, similar to:

![Screenshot showing error produced by completion-closure not being called](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2018/06/completion-handler-never-called-error.png)

So far, `BackgroundDownloadService` only works when the app is in the foreground. Let's extend it to support downloads when the app enters a suspended/terminated state.

## Keeping downloads going

When an app is in a suspended/terminated state and a download completes, iOS will wake the app up (in the background) by calling `application(_:handleEventsForBackgroundURLSession:completionHandler:)` on the `AppDelegate`. Regardless of how many downloads are in progress, `application(_:handleEventsForBackgroundURLSession:completionHandler:)` is only called **once** \- when **all** downloads have been completed. If the user foregrounds the app before the above method is called, `urlSession(_:downloadTask:didFinishDownloadingTo:)` is called for each completed download.

```
class AppDelegate: UIResponder, UIApplicationDelegate {
    // Omitted properties and methods

    // 1
    func application(_ application: UIApplication,
                     handleEventsForBackgroundURLSession identifier: String,
                     completionHandler: @escaping () -> Void) {
        // 2
        BackgroundDownloadService.shared.backgroundCompletionHandler = completionHandler
    }
}
```

Here's what we did:

1. iOS provides the app with the identifier of the session that the download was scheduled on and a completion handler to be called once all post-download work is complete. As the background session always uses the same identifier, `identifier` can be ignored.
2. `completionHandler` is passed to `BackgroundDownloadService` so that it can be called once all post-download work is complete.

As the `completionHandler` closure is passed to the shared `BackgroundDownloadService` instance, a new property needs to be added to that class:

```
class BackgroundDownloadService: NSObject {
    var backgroundCompletionHandler: (() -> Void)?

    // Omitted properties and methods
}
```

When all the downloads are complete, `urlSession(_:downloadTask:didFinishDownloadingTo:)` will be called for **each** download. After all the calls to `urlSession(_:downloadTask:didFinishDownloadingTo:)` have been made, a special background-transfer only delegate call is made:

```
extension BackgroundDownloadService: URLSessionDownloadDelegate {
    // Omitted methods

    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        // 1
        DispatchQueue.main.async {
            self.backgroundCompletionHandler?()
            self.backgroundCompletionHandler = nil
        }
    }
}
```

Here's what we did:

1. Triggering `backgroundCompletionHandler` instructs iOS to take a new snapshot of the app's UI for the [app switcher preview](https://support.apple.com/en-gb/HT202070?ref=williamboles.com). As such, `backgroundCompletionHandler` must be called from the main queue. Failure to call `backgroundCompletionHandler` will result in iOS treating the app as a *bad citizen*, reducing future background processing opportunities.

> While it's possible to make additional network requests with the app running in the background, they are subject to a rate limiter to [prevent abuse of background sessions](https://developer.apple.com/documentation/foundation/url%5Floading%5Fsystem/downloading%5Ffiles%5Fin%5Fthe%5Fbackground/?ref=williamboles.com#3038438) \- this rate limiter delays the start of those network requests. With each separate network request, the delay increases - this delay is reset when the user brings the app to the foreground or if your app does not make any additional network requests during the delay.

And that's all the changes required to continue network requests when the app is suspended/terminated.

## Living in a multi-threaded world

Using the code we've written so far on a single thread works fine, but using it simultaneously from multiple threads will eventually cause the app to crash. When accessing `BackgroundDownloadStore` from multiple threads, one thread may be modifying the internal mutable state of `BackgroundDownloadStore` while another thread is also modifying it. As `BackgroundDownloadStore` doesn't lock its state during a modification operation, simultaneous modification operations will eventually result in data corruption and the app crashing.

Thoughtful use of a [DispatchQueue](https://developer.apple.com/documentation/dispatch/dispatchqueue?ref=williamboles.com) can prevent these issues. Our `DispatchQueue` instance will allow concurrent *read* operations but serial *write* operations. If a *write* operation is being processed, any new operation (*write* or *read*) added to the queue will be blocked until the in-progress *write* operation completes. Using a `DispatchQueue` instance will ensure that only one *write* operation runs at a time, preventing data corruption and stopping the app crashes we are currently experiencing.

Let's add state access controls to `BackgroundDownloadStore` to protect its shared mutable state:

```
class BackgroundDownloadStore {
    // 1
    private let queue = DispatchQueue(label: "com.williamboles.background.download.service",
                                      qos: .userInitiated,
                                      attributes: .concurrent)

    // Omitted properties and methods
}
```

Here's what we did:

1. As the downloads are triggered via user action, the queue priority is set to the higher end of the scale by setting its `qos` to `userInitiated`; this will ensure that operations on this queue are given greater resources when compared to lower priority operations on other queues. By default, an instance of `DispatchQueue` is a serial queue; this default is overridden by creating a concurrent queue using the `concurrent` attribute.

With the above `queue` property, let's update the rest of `BackgroundDownloadStore`, starting with the `storeMetadata(from:to:completionHandler:)` method:

```
class BackgroundDownloadStore {
    // Omitted properties and methods

    func storeMetadata(from fromURL: URL,
                       to toURL: URL,
                       completionHandler: @escaping BackgroundDownloadCompletion) {
        // 1
        queue.async(flags: .barrier) { [weak self] in
            self?.inMemoryStore[fromURL.absoluteString] = completionHandler
            self?.persistentStore.set(toURL, forKey: fromURL.absoluteString)
        }
    }
}
```

Here's what we did:

1. Using our `DispatchQueue` instance, we **asynchronously** schedule the metadata storage to happen on that queue. Adding the operation asynchronously ensures that the calling thread won't be blocked. As this is a *write* operation, the [barrier](https://developer.apple.com/documentation/dispatch/dispatch-barrier?ref=williamboles.com) flag is set when adding the operation to `queue`; this flag will switch the dispatch queue from concurrent processing to serial processing for this operation.

The changes needed to protect `removeMetadata(for:)` are almost the same as those made for `storeMetadata(from:to:completionHandler:)`:

```
class BackgroundDownloadStore {
    // Omitted properties and methods

    func removeMetadata(for forURL: URL) {
        queue.async(flags: .barrier) { [weak self] in
            let key = forURL.absoluteString

            self?.inMemoryStore[key] = nil
            self?.persistentStore.removeObject(forKey: key)
        }
    }
}
```

`retrieveMetadata(for:)` requires slightly more change as it needs to return a download's metadata without blocking the calling thread. The best way to achieve this is to return its result via a closure:

```
class BackgroundDownloadStore {
    // Omitted properties and methods

    func retrieveMetadata(for forURL: URL,
                          completionHandler: @escaping ((URL?, BackgroundDownloadCompletion?) -> ())) {
        // 1
        return queue.async { [weak self] in
            let key = forURL.absoluteString

            let toURL = self?.persistentStore.url(forKey: key)
            let metaDataCompletionHandler = self?.inMemoryStore[key]

            completionHandler(toURL, metaDataCompletionHandler)
        }
    }
}
```

Here's what we did:

1. The retrieval of the metadata associated with `forURL` is **asynchronously** scheduled on `queue`. Adding the retrieval operation asynchronously ensures that the calling thread won't be blocked. As this is a *read* operation, the operation can run concurrently with any other *read* operations, so unlike in `storeMetadata(from:to:completionHandler:)` and `removeMetadata(for:)`, the operation is **not** flagged as a `barrier` operation.

> We could have added the *read* operation as a synchronous operation to the dispatch queue using `queue.sync`, as we know that `BackgroundDownloadService` will call this method from a background thread, and so avoid having to add a closure to return the result of the retrieval. However, I felt that adding a blocking thread call here would lead to a nasty surprise if `retrieveMetadata(for:completionHandler:)` was ever used on the main thread.

With the changes to the interface of `BackgroundDownloadStore` to ensure thread safety, `BackgroundDownloadService` needs to be updated:

```
extension BackgroundDownloadService: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession,
                    downloadTask: URLSessionDownloadTask,
                    didFinishDownloadingTo location: URL) {
        guard let fromURL = downloadTask.originalRequest?.url else {
            os_log(.error, "Unexpected nil URL")
            // Unable to call the closure here as fromURL is the key to retrieving this download's closure
            return
        }

        let fromURLAsString = fromURL.absoluteString

        os_log(.info, "Download request completed for: %{public}@", fromURLAsString)

        // 1
        let tempLocation = FileManager.default.temporaryDirectory.appendingPathComponent(location.lastPathComponent)
        try? FileManager.default.moveItem(at: location,
                                          to: tempLocation)

        // 2
        store.retrieveMetadata(for: fromURL) { [weak self] toURL, completionHandler in
            // 3
            defer {
                self?.store.removeMetadata(for: fromURL)
            }

            guard let toURL else {
                os_log(.error, "Unable to find existing download item for: %{public}@", fromURLAsString)
                completionHandler?(.failure(BackgroundDownloadError.missingInstructionsError))
                return
            }

            guard let response = downloadTask.response as? HTTPURLResponse,
                        response.statusCode == 200 else {
                os_log(.error, "Unexpected response for: %{public}@", fromURLAsString)
                completionHandler?(.failure(BackgroundDownloadError.serverError(downloadTask.response)))
                return
            }

            os_log(.info, "Download successful for: %{public}@", fromURLAsString)

            do {
                // 4
                try FileManager.default.moveItem(at: tempLocation,
                                                 to: toURL)

                completionHandler?(.success(toURL))
            } catch {
                completionHandler?(.failure(BackgroundDownloadError.fileSystemError(error)))
            }
        }
    }

   // Omitted other methods
}
```

Here's what we changed:

1. As mentioned before, iOS only guarantees until the end of this method that the downloaded content will be found at `location`. As `retrieveMetadata(for:completionHandler:)` is an asynchronous method, waiting on the completion handler being called with the download's metadata will mean that `urlSession(_:downloadTask:didFinishDownloadingTo)` will have exited and the downloaded content will have been deleted. So before attempting to fetch the download's metadata, the downloaded content is moved from its current temporary location to a different temporary location that `BackgroundDownloadService` controls. Moving to a temporary location ensures that the downloaded content will still be accessible when the completion handler calls back with the metadata.
2. Using the closure-based interface for retrieving the completed download's metadata.
3. Moved the `defer` block into the closure so that it is associated with the closure block rather than the method itself.
4. Using the `tempLocation` URL as the source URL for the move operation.

And finally, let's use the same pattern with `urlSession(_:task:didCompleteWithError)`

```
extension BackgroundDownloadService: URLSessionDownloadDelegate {
    // Omitted other methods

    func urlSession(_ session: URLSession,
                       task: URLSessionTask,
                       didCompleteWithError error: Error?) {
         guard let error = error else {
             return
         }

         guard let fromURL = task.originalRequest?.url else {
             os_log(.error, "Unexpected nil URL")
             return
         }

         let fromURLAaString = fromURL.absoluteString

         os_log(.info, "Download failed for: %{public}@", fromURLAaString)

         // 1
         store.retrieveMetadata(for: fromURL) { [weak self] _, completionHandler in
             completionHandler?(.failure(BackgroundDownloadError.clientError(error)))

             self?.store.removeMetadata(for: fromURL)
         }
     }
}
```

Here's what we did:

1. Using the closure-based interface for retrieving the completed download's metadata.

If you run the project now in a multi-threaded app, downloads should complete without app crashes due to data corruption.

And that's all the code needed to support background-transfers 💃.

However, there is one gotcha 🧌 when testing the above code. If an app is manually force-quit, iOS interprets that action as the user explicitly saying that they want to stop all activity associated with that app. As a result, all scheduled background-transfers are cancelled, preventing testing of the restoration functionality. To overcome this gotcha, app termination needs to happen programmatically using `exit(0)`, which shuts down the app **without** user intervention:

```
class AppDelegate: UIResponder, UIApplicationDelegate {
    // Omitted properties and methods

    func applicationDidEnterBackground(_ application: UIApplication) {
        //Exit the app to test restoring the app from a terminated state. Comment out to test restoring the app from a suspended state.
        DispatchQueue.main.asyncAfter(deadline: .now()) {
            os_log(.info, "App is about to quit")

            exit(0)
        }
    }
}
```

> Remember to remove this in production 😜.

This time we are really at the end 🎉 - congratulations on making it here.

## Downloads keep going 🤯

Background-transfers provide a powerful tool to meet your users' ever-increasing expectations about what your app can do for them, even when your app isn't in the foreground. I believe that the enhanced user experience we gain from supporting background-transfers outweighs any additional complexity that supporting background-transfers adds. As we have seen with careful planning, that complexity can be isolated from the rest of the app, so we end up providing functionality without getting in the way of features.

To see the complete working example, visit the [repository](https://github.com/wibosco/BackgroundTransfer-Example?ref=williamboles.com) and clone the project.

---

## Running the example project

I've used [TheCatAPI](https://thecatapi.com/?ref=williamboles.com) in the example project to populate the app with images. `TheCatAPI` has a great JSON-based API and an extensive library of lovely, freely available cat photos. While free to use, `TheCatAPI` requires you to [register](https://thecatapi.com/signup?ref=williamboles.com) to get an `x-api-key` token to access those cats. You must add this token as the `APIKey` property in `NetworkService` for any network requests to work.