> ## 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.

# Keep Downloading with a Background Session
- URL: https://williamboles.com/keep-downloading-with-a-background-session/
- Published: 2025-04-29T09:36:21.000Z
- Updated: 2026-09-26T20:43:36.000Z
- Description: Users always want more. With a background URLSession, we can give them more by extending downloads and uploads beyond the app's lifespan. Here, we look at building a solution to take advantage of a background session to download adorable cat photos 😸.
- Author: William Boles
- Tags: Networking, Concurrency, The Cat API

In the distant past, when dinosaurs still roamed the land, 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, as the years passed, devices have become more powerful and energy-efficient. Unlike our prehistoric predecessors 🦖, iOS adapted to meet its changing environment. iOS now allows apps to perform limited background work while maintaining foreground responsiveness.

One form of permitted background work is being able to continue download or upload network requests when an app has been suspended or even terminated. These network requests are collectively known as `background-transfers` \- `background` being where the network request happens and `transfer` being the collective term for download and upload requests. Support for background-transfers was introduced in iOS 7\. While not a recent change, it remains a powerful tool.

This post will explore background-transfers and how we can enhance the overall experience of our apps by adding support for them.

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

> 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/BackgroundTransferRevised-Example?ref=williamboles.com) and take a look at `BackgroundDownloadService` and `AppDelegate` 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 the [NSURLConnection](https://developer.apple.com/documentation/foundation/nsurlconnection?ref=williamboles.com) networking stack. One particular pain point of an `NSURLConnection` networking stack was that 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. Requests scheduled within a session automatically inherit its 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.

There are three flavours of session:

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 own use case; let's explore how we can use a background session to enable our transfers 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 adding value.
- **Programming Complexity** \- implementing background-transfers requires forgoing the convenient async/await and/or closure-based methods of `URLSession` and instead conforming to [URLSessionDownloadDelegate](https://developer.apple.com/documentation/foundation/urlsessiondownloaddelegate?ref=williamboles.com) to receive updates about the transfers, adding complexity when compared to foreground-only sessions.

> You may have watched [Efficiency awaits: Background tasks in SwiftUI](https://developer.apple.com/videos/play/wwdc2022/10142/?ref=williamboles.com) from WWDC22 and be thinking that you can use the convenient async/await and/or closure-based methods; however, if you do so, **your app will crash**. The functionality shown in that walkthrough sadly never made it into production as described in this Apple Developer forum [thread](https://developer.apple.com/forums/thread/762921?ref=williamboles.com).

Now that we know more about background-transfers, let's see how we can add support for background-transfers to our project.

## 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 configuring 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, let's look at how to perform a background download.

## 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.

These responsibilities are handled inside `BackgroundDownloadService`. As `URLSessionDownloadDelegate` mostly doesn't differentiate between downloads that complete when the app is in the **foregrounded**, **suspended** or **terminated** state, all three possible states need to be handled together.

The steps involved in handling a download that completes with the app in the foreground state are slightly different from a download that completes with the app in the suspended/terminated state:

| Foregrounded                                                                                         | Suspended/Terminated                                                                                               |
| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| A download is requested on BackgroundDownloadService.                                                | A download is requested on BackgroundDownloadService.                                                              |
| The requested download's metadata is stored.                                                         | The requested download's metadata is stored.                                                                       |
| The download is kicked off.                                                                          | The download is kicked off.                                                                                        |
| BackgroundDownloadService is informed of the outcome of the download via URLSessionDownloadDelegate. | The app is woken up, and the app-switcher-preview closure is passed from AppDelegate to BackgroundDownloadService. |
| Metadata is retrieved for the download.                                                              | BackgroundDownloadService is informed of the outcome of the download via URLSessionDownloadDelegate.               |
| The downloaded content is moved to its permanent local file system location.                         | Metadata is retrieved for the download.                                                                            |
| Continuation is resumed.                                                                             | The downloaded content is moved to its permanent local file system location.                                       |
|                                                                                                      | BackgroundDownloadService is informed that all downloads are completed.                                            |
|                                                                                                      | App-switcher-preview closure is triggered.                                                                         |

> Don't worry if any part of the above steps doesn't make sense; each part will be explored in more depth as the solution is built up.

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

```
enum BackgroundDownloadError: Error {
    case cancelled
    case fileSystemError(_ underlyingError: Error)
    case clientError(_ underlyingError: Error)
    case serverError(_ underlyingResponse: URLResponse?)
}
```

`BackgroundDownloadError` 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`:

```
// 1
actor BackgroundDownloadService: NSObject {
    // 2
    nonisolated(unsafe) private var session: URLSession!

    // MARK: - Singleton

    // 3
    static let shared = BackgroundDownloadService()

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

        // 5
        let configuration = URLSessionConfiguration.background(withIdentifier: "com.williamboles.background.download.session")
        // 6
        configuration.isDiscretionary = false
        // 7
        configuration.sessionSendsLaunchEvents = true

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

Here's what we did:

1. As mentioned earlier, background session downloads don't support the [async/await](https://developer.apple.com/documentation/foundation/urlsession/download%28for:delegate:%29?ref=williamboles.com) or [closure](https://developer.apple.com/documentation/foundation/urlsession/1411511-downloadtask?ref=williamboles.com) methods on `URLSession`; instead, updates are provided via the session's delegate. `BackgroundDownloadService` needs to be a subclass of [NSObject](https://developer.apple.com/documentation/objectivec/nsobject-swift.class?ref=williamboles.com) to conform to `URLSessionDownloadDelegate` (we will add conformance to `URLSessionDownloadDelegate` later in an extension).
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`. To break this cyclic dependency, `session` is declared as an implicitly unwrapped optional (`URLSession!`) and marked as [nonisolated(unsafe)](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md?ref=williamboles.com) \- opting it out of the actor's mutual exclusion guarantees. This allows `session` to be assigned inside `init` after `super.init()` has completed, at which point `self` is fully initialised and can be passed as the delegate. Once assigned in `init`, `session` is never mutated again, so the `unsafe` opt-out carries no practical risk.
3. `BackgroundDownloadService` is a singleton to ensure that any background download updates are funnelled into the same type that holds the metadata of that download.
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. Setting `isDiscretionary` to `false` tells iOS that any downloads scheduled on this session should be started as soon as possible, rather than at the discretion of iOS.
7. Setting `sessionSendsLaunchEvents` to `true` tells iOS that any downloads scheduled on this session can launch the app if it is suspended/terminated.

> Initially I used a [lazy property](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/?ref=williamboles.com#Lazy-Stored-Properties) to break the cyclic dependency between `BackgroundDownloadService` and `URLSession` by delaying the creation of our `URLSession` instance until its first usage. However, as we will see later, `BackgroundDownloadService` and `URLSession` need to share the same lifecycle - when one is created, so must the other be - otherwise when the app is woken after termination, downloads can be left hanging.

So far, we have configured a session for performing background downloads, but we can't yet download anything.

Two pieces of information are required for each download:

1. **Remote URL** \- where the content to be downloaded is.
2. **File path URL** \- where the downloaded content should be stored on the local file system.

Where we kick off a download and where we handle the outcome of that download are split over a delegate callback. So we need a way to cache the **file path URL** when that download is kicked off and then retrieve it when that download completes:

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

    // 1
    private let persistentStore = UserDefaults.standard
}
```

1. As a download may complete after the app has been terminated and relaunched, 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. The **remote URL** will be the key, with the **file path URL** being the value.

As mentioned, background sessions don't support the async/await methods on `URLSession`. However, an async/await based interface is perfect for downloading content where the caller is really only interested in having the downloaded content or an error. We can recreate that convenient async/await interface by making use of a [continuation](https://developer.apple.com/documentation/swift/checkedcontinuation?ref=williamboles.com) to transform the delegate-based approach into an async/await approach:

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

    // 1
    private var inMemoryStore = [String: CheckedContinuation<URL, Error>]()

    // MARK: - Download

    func download(from fromURL: URL,
                  to toURL: URL) async throws -> URL {
        // 2
        return try await withCheckedThrowingContinuation { continuation in
            // 3
            inMemoryStore[fromURL.absoluteString] = continuation
            persistentStore.set(toURL, forKey: fromURL.absoluteString)

            // 4
            let downloadTask = session.downloadTask(with: fromURL)
            downloadTask.resume()
        }
    }
}
```

1. Just like caching the **file path URL**, the continuation associated with the download needs to be cached to be resumed later. The continuation 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 unlike with the **file path URL** storage, the storage of a continuation is only in memory, as there is nothing useful to persist. The **remote URL** will act as the key.
2. To transform the delegate-based approach into an async/await approach, the download kickoff is wrapped in a [withCheckedThrowingContinuation(isolation:function:\_:)](https://developer.apple.com/documentation/swift/withcheckedthrowingcontinuation%28isolation:function:%5F:%29?ref=williamboles.com) block.
3. The metadata associated with this download is stored.
4. 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: 
> 
> ```
> downloadTask.earliestBeginDate = Date().addingTimeInterval(5)
> ```

While it is currently possible to add breakpoints to determine what is happening in `BackgroundDownloadService`, as we will see when implementing the app relaunch behaviour, breakpoints will only get us so far. Instead of waiting until then, let's add logging to `BackgroundDownloadService` just now to log what is happening:

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

    // 1
    private let logger = Logger(subsystem: "com.williamboles",
                                category: "background.download")

    // MARK: - Download

    func download(from fromURL: URL,
                  to toURL: URL) async throws -> URL {
        return try await withCheckedThrowingContinuation { continuation in
            // 2
            logger.info("Scheduling download: \(fromURL.absoluteString)")

            inMemoryStore[fromURL.absoluteString] = continuation
            persistentStore.set(toURL, forKey: fromURL.absoluteString)

            let downloadTask = session.downloadTask(with: fromURL)
            downloadTask.resume()
        }
    }
}
```

1. Apple recommends using the [unified logging system](https://developer.apple.com/documentation/os/generating-log-messages-from-your-code?ref=williamboles.com) to log events over something like `print` or `NSLog`. [Logger](https://developer.apple.com/documentation/os/logger?ref=williamboles.com) writes string messages to that unified logging system. Here, we are setting the `subsystem` and `category` values, allowing us to filter the logs to see only the messages that interest us.
2. Logging an event using the `info` [log level](https://developer.apple.com/documentation/os/oslogtype?ref=williamboles.com).

> If you are curious to know more, Antoine van der Lee has written an excellent [post](https://www.avanderlee.com/debugging/oslog-unified-logging/?ref=williamboles.com) on the unified logging system.

Now that `BackgroundDownloadService` can kick off a download, it needs to conform to `URLSessionDownloadDelegate` to receive updates about that download. As `BackgroundDownloadService` is of type `actor`, conforming to `URLSessionDownloadDelegate` is slightly more complex than it would be for a `class` type. By default, an `actor` enforces mutual exclusion when accessing its state, resulting in those accesses needing to be performed asynchronously from outside the `actor`. However, `URLSession` expects synchronous access to its delegate. At first glance, it looks like `BackgroundDownloadService` can't conform to `URLSessionDownloadDelegate`; however, what appears impossible at first glance isn't. An `actor` can have `nonisolated` methods. A `nonisolated` method, while defined within an `actor`, is outside its mutual exclusion zone, meaning it can be accessed synchronously - essentially, acting like a regular class method. By marking the `URLSessionDownloadDelegate` methods as `nonisolated`, `BackgroundDownloadService` can act as the delegate to its session - with one caveat: any work that needs to access the state of `BackgroundDownloadService` needs to be moved into the mutual exclusion zone rather than handled in the `nonisolated` method.

Let's implement the happy-path method of `urlSession(_:downloadTask:didFinishDownloadingTo)` from `URLSessionDownloadDelegate`:

```
// 1
extension BackgroundDownloadService: URLSessionDownloadDelegate {
    // MARK: - URLSessionDownloadDelegate

    // 2
    nonisolated
    func urlSession(_ session: URLSession,
                    downloadTask: URLSessionDownloadTask,
                    didFinishDownloadingTo location: URL) {

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

        // 4
        Task {
            await downloadFinished(task: downloadTask,
                                   downloadedTo: tempLocation)
        }
    }
}
```

1. `BackgroundDownloadService` conforms to `URLSessionDownloadDelegate` in an extension.
2. `urlSession(_:downloadTask:didFinishDownloadingTo)` is marked as a `nonisolated` method to allow synchronous access.
3. 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) \- `location`. iOS only guarantees until the end of this method that the downloaded content will be found at `location`. As the real work of completing a download will occur in `downloadFinished(task:downloadedTo:)`, which is accessed asynchronously via a `Task` waiting on `downloadFinished(task:downloadedTo:)` being called before moving the downloaded content will mean that `urlSession(_:downloadTask:didFinishDownloadingTo)` will have exited and the downloaded content will have been deleted. So before making that asynchronous call, the downloaded content is moved from its current temporary location to a different temporary location that `BackgroundDownloadService` controls to ensure that it will still be there for `downloadFinished(task:downloadedTo:)` to work with.
4. The call to `downloadFinished(task:downloadedTo:)` is wrapped in a `Task` as it exists within the mutual exclusion zone of `BackgroundDownloadService`, which `urlSession(_:downloadTask:didFinishDownloadingTo)` is not in, so `downloadFinished(task:downloadedTo:)` is treated as an `async` method.

Let's see what `downloadFinished(task:downloadedTo:)` does:

```
actor BackgroundDownloadService: NSObject {
    // Omitted properties and other methods

    private func downloadFinished(task: URLSessionDownloadTask,
                                  downloadedTo location: URL) {

        // 1
        guard let fromURL = task.originalRequest?.url else {
            logger.error("Unexpected nil URL for download task.")
            return
        }

        logger.info("Download request completed for: \(fromURL.absoluteString)")

        // 2
        defer {
            cleanUpDownload(forURL: fromURL)
        }

        // 3
        guard let toURL = persistentStore.url(forKey: fromURL.absoluteString) else {
            logger.error("Unable to find existing download for: \(fromURL.absoluteString)")
            return
        }

        // 4
        let continuation = inMemoryStore[fromURL.absoluteString]

        logger.info("Download successful for: \(fromURL.absoluteString)")

        // 5
        do {
            try FileManager.default.moveItem(at: location,
                                             to: toURL)
            // 6
            continuation?.resume(returning: toURL)
        } catch {
            logger.error("File system error while moving file: \(error.localizedDescription)")
            // 7
            continuation?.resume(throwing: BackgroundDownloadError.fileSystemError(error))
        }
    }

    // MARK: - Cleanup

    // 8
    private func cleanUpDownload(forURL url: URL) {
        inMemoryStore.removeValue(forKey: url.absoluteString)
        persistentStore.removeObject(forKey: url.absoluteString)
    }
}
```

This 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(from:to:)` method on `BackgroundDownloadService`, the `fromURL` was used as the key for storing a download's metadata because that value is present when a download completes in the `URLSessionDownloadTask` instance. Here, we extract that URL from `URLSessionDownloadTask`. It's important to note that 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 exits from `downloadFinished(task:downloadedTo:)`, a `defer` block is used to clean up the download by deleting the download's associated metadata on any exit.
3. 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.
4. Unlike `toURL`, there is no hard requirement for a `continuation` to be present, so no check is made to ensure it is not `nil`.
5. 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.
6. The associated continuation is resumed with the file's permanent location returned.
7. The associated continuation is resumed with an error being thrown.
8. The metadata associated with this download is deleted. This deletion will occur in several methods, so it is best to extract it now.

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 `downloadFinished(task:downloadedTo:)`:

```
actor BackgroundDownloadService: NSObject {
    // Omitted properties and other methods

    private func downloadFinished(task: URLSessionDownloadTask,
                                  downloadedTo location: URL) {
        guard let fromURL = task.originalRequest?.url else {
            logger.error("Unexpected nil URL for download task.")
            return
        }

        logger.info("Download request completed for: \(fromURL.absoluteString)")

        defer {
            cleanUpDownload(forURL: fromURL)
        }

        guard let toURL = persistentStore.url(forKey: fromURL.absoluteString) else {
            logger.error("Unable to find existing download for: \(fromURL.absoluteString)")
            return
        }

        let continuation = inMemoryStore[fromURL.absoluteString]

        // 1
        guard let response = task.response as? HTTPURLResponse,
              response.statusCode == 200 else {
            logger.error("Unexpected response for: \(fromURL.absoluteString)")
            continuation?.resume(throwing: BackgroundDownloadError.serverError(task.response))
            return
        }

        // Omitted the rest of the method
    }
}
```

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 `continuation` is resumed 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 handle the client-side errors. Let's implement the unhappy-path method of `urlSession(_:task:didCompleteWithError:)` from `URLSessionDownloadDelegate`:

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

    // 1
    nonisolated
    func urlSession(_ session: URLSession,
                    task: URLSessionTask,
                    didCompleteWithError error: Error?) {
        // 2
        Task {
            await downloadComplete(task: task,
                                   withError: error)
        }
    }
}
```

Here's what we changed:

1. Just like with `urlSession(_:downloadTask:didFinishDownloadingTo)`, `urlSession(_:task:didCompleteWithError:)` is marked as a `nonisolated` method to allow synchronous access.
2. The call to `downloadComplete(task:withError:)` is wrapped in a `Task` as it exists within the mutual exclusion zone of `BackgroundDownloadService`, which `urlSession(_:task:didCompleteWithError:)` is not in, so `downloadComplete(task:withError:)` is treated as an `async` method.

Let's see what `downloadComplete(task:withError:)` does:

```
actor BackgroundDownloadService: NSObject {
    // Omitted properties and other methods

    private func downloadComplete(task: URLSessionTask,
                                  withError error: Error?) {
        // 1
        guard let error = error else {
            return
        }

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

        logger.info("Download failed for: \(fromURL.absoluteString), error: \(error.localizedDescription)")

        let continuation = inMemoryStore[fromURL.absoluteString]

        // 3
        continuation?.resume(throwing: BackgroundDownloadError.clientError(error))

        // 4
        cleanUpDownload(forURL: fromURL)
    }
}
```

1. `downloadComplete(task:withError:)` 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 `downloadFinished(task:downloadedTo:)`, 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 associated continuation is resumed with an error being thrown.
4. The metadata associated with this download is deleted.

If you've been putting together the code snippets, that's all you need to do to use a background session to download files in the foreground; however, we still have work to do to support relaunching the app to complete a download.

## 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`. This method accepts a closure `completionHandler` that needs to be triggered once we have completed post-download processing for all background downloads. Triggering `completionHandler` 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). Failure to call `completionHandler` will result in iOS treating the app as a *bad citizen*, reducing future background processing opportunities. 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.

**Depending on how your app is set up, open the section relevant to your project:**

**SwiftUI**   

Add the following code to your `AppDelegate`:

```
class AppDelegate: NSObject, UIApplicationDelegate {
    // 1
    private var completionHandler: (() -> Void)?

    // MARK: - UIApplicationDelegate

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

        // 2
        Task {

            // 3
            await BackgroundDownloadService.shared.setAppPreviewCompletionHandler { [weak self] in

                // 4
                Task { @MainActor in
                    self?.backgroundDownloadsComplete()
                }
            }
        }
    }

    // 5
    private func backgroundDownloadsComplete() {
        completionHandler?()
        completionHandler = nil
    }
}
```

> If your app doesn't already have an AppDelegate, you will need to add it - SwiftUI apps don't have one by default.

1. The system-provided completion handler is held onto rather than acted on immediately. When iOS wakes the app to process background session events, it hands over this handler and expects it to be called once that processing is done - until then, it needs somewhere to live. Keeping it as a property on `AppDelegate`, on the main actor, means it never has to cross into another isolation domain.
2. As `BackgroundDownloadService` is an `actor`, interacting with it from the outside is asynchronous - we wrap that interaction in a `Task` to allow an asynchronous call to happen within a synchronous method. As `application(_:handleEventsForBackgroundURLSession:completionHandler:)` is main actor-isolated, this `Task` inherits the main actor, ensuring `completionHandler` is only ever touched from a single isolation domain.
3. `BackgroundDownloadService` operates in a different concurrency-context from `AppDelegate` (which is on the main actor), so we can only pass data between those contexts if that data is [Sendable](https://developer.apple.com/documentation/Swift/Sendable?ref=williamboles.com) \- and `completionHandler` isn't a `Sendable` closure. To know when `BackgroundDownloadService` has finished its work, we instead pass in a different closure that *is* `Sendable`. When triggered, it calls `completionHandler` from within the concurrency-context of `AppDelegate`. `[weak self]` stops the `actor`\-retained closure from keeping `AppDelegate` alive.
4. When `BackgroundDownloadService` fires the closure, control is hopped back onto the main actor.
5. The stored handler is called to tell iOS that the app has finished handling its background events, before being set to `nil` so that it can't be accidentally called twice and so that a later relaunch starts from a clean slate.

> The rule that only `Sendable` data can cross between concurrency-contexts is the right mental model to hold, but it isn't the whole story. Under [region-based isolation](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md?ref=williamboles.com) (introduced in Swift 6), the compiler can sometimes allow a non-`Sendable` value to cross an `actor` boundary if it can prove that the value won't be used again on the sending side - the value is said to be in a *disconnected* region, leaving one context with no lingering references for the other to race against. That isn't an option for us here: `completionHandler` is captured and stored as a property, so it remains reachable from `AppDelegate` after being passed in - exactly the lingering reference that region-based isolation needs to rule out.

Now we need to integrate `AppDelegate` into the `App`:

```
@main
struct BackgroundTransferRevised_ExampleApp: App {
    // 1
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    // Omitted other properties
}
```

> `BackgroundTransferRevised_ExampleApp` is the name of the example project on which this post is based.

1. Using the [@UIApplicationDelegateAdaptor](https://developer.apple.com/documentation/swiftui/uiapplicationdelegateadaptor?ref=williamboles.com) property wrapper to set our custom app delegate.
**UIKit**   

Add the following code to your `AppDelegate`:

```
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    // 1
    private var completionHandler: (() -> Void)?

    // MARK: - UIApplicationDelegate

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

        // 2
        Task {

            // 3
            await BackgroundDownloadService.shared.setAppPreviewCompletionHandler { [weak self] in

                // 4
                Task { @MainActor in
                    self?.backgroundDownloadsComplete()
                }
            }
        }
    }

    // 5
    private func backgroundDownloadsComplete() {
        completionHandler?()
        completionHandler = nil
    }
}
```

1. The system-provided completion handler is held onto rather than acted on immediately. When iOS wakes the app to process background session events, it hands over this handler and expects it to be called once that processing is done - until then, it needs somewhere to live. Keeping it as a property on `AppDelegate`, on the main actor, means it never has to cross into another isolation domain.
2. As `BackgroundDownloadService` is an `actor`, interacting with it from the outside is asynchronous - we wrap that interaction in a `Task` to allow an asynchronous call to happen within a synchronous method. As `application(_:handleEventsForBackgroundURLSession:completionHandler:)` is main actor-isolated, this `Task` inherits the main actor, ensuring `completionHandler` is only ever touched from a single isolation domain.
3. `BackgroundDownloadService` operates in a different concurrency-context from `AppDelegate` (which is on the main actor), so we can only pass data between those contexts if that data is [Sendable](https://developer.apple.com/documentation/Swift/Sendable?ref=williamboles.com) \- and `completionHandler` isn't a `Sendable` closure. To know when `BackgroundDownloadService` has finished its work, we instead pass in a different closure that *is* `Sendable`. When triggered, it calls `completionHandler` from within the concurrency-context of `AppDelegate`. `[weak self]` stops the `actor`\-retained closure from keeping `AppDelegate` alive.
4. When `BackgroundDownloadService` fires the closure, control is hopped back onto the main actor.
5. The stored handler is called to tell iOS that the app has finished handling its background events, before being set to `nil` so that it can't be accidentally called twice and so that a later relaunch starts from a clean slate.

> The rule that only `Sendable` data can cross between concurrency-contexts is the right mental model to hold, but it isn't the whole story. Under [region-based isolation](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md?ref=williamboles.com) (introduced in Swift 6), the compiler can sometimes allow a non-`Sendable` value to cross an `actor` boundary if it can prove that the value won't be used again on the sending side - the value is said to be in a *disconnected* region, leaving one context with no lingering references for the other to race against. That isn't an option for us here: `completionHandler` is captured and stored as a property, so it remains reachable from `AppDelegate` after being passed in - exactly the lingering reference that region-based isolation needs to rule out.

  
Regardless of how we got here, the `completionHandler` closure has been passed to `BackgroundDownloadService`, which has then stored it as a property:

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

    private var appPreviewCompletionHandler: (@Sendable () -> Void)?

    // Omitted other methods

    func setAppPreviewCompletionHandler(_ appPreviewCompletionHandler: @escaping (@Sendable () -> Void)) {
        self.appPreviewCompletionHandler = appPreviewCompletionHandler
    }
}
```

Just like when the app is in the foreground, the `URLSessionDownloadDelegate` method: `urlSession(_:downloadTask:didFinishDownloadingTo:)` is called for each download; however, unlike when the app is in the foreground, there is an additional background-transfer only delegate call made after the final `urlSession(_:downloadTask:didFinishDownloadingTo:)` call:

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

    // 1
    nonisolated
    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        // 2
        Task {
            await backgroundDownloadsComplete()
        }
    }
}
```

1. Just like with the other `URLSessionDownloadDelegate` methods, `urlSessionDidFinishEvents(forBackgroundURLSession:)` is marked as `nonisolated` method to allow synchronous access.
2. The call to `backgroundDownloadsComplete()` is wrapped in a `Task` as it exists within the mutual exclusion zone of `BackgroundDownloadService`, which `urlSessionDidFinishEvents(forBackgroundURLSession:)` is not in, so `backgroundDownloadsComplete()` is treated as an `async` method.

Let's see what `backgroundDownloadsComplete()` does:

```
actor BackgroundDownloadService: NSObject {
    // Omitted properties and other methods

    private func backgroundDownloadsComplete() {
        logger.info("All background downloads completed")

        // 1
        appPreviewCompletionHandler?()
        appPreviewCompletionHandler = nil
    }
}
```

1. `appPreviewCompletionHandler` is triggered and then set to `nil` to avoid it accidentally being triggered again.

> 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.

The code has been added to handle the app being relaunched when suspended/terminated, but it is important to verify that the implementation works as intended. When an app is manually force-quit, iOS interprets this as the user explicitly indicating a desire to halt all activity related to the app. Consequently, all scheduled background transfers are cancelled, which prevents 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.

**Depending on how your app is set up, open the section relevant to your project:**

**SwiftUI**   

```
@main
struct BackgroundTransferRevised_ExampleApp: App {
    // Omitted other properties

    @Environment(\.scenePhase) private var scenePhase

    private let logger = Logger(subsystem: "com.williamboles",
                                category: "app")

    // MARK: - Scene

    var body: some Scene {
        WindowGroup {
            // Omitted statements
        }
        .onChange(of: scenePhase) { (_, newPhase) in
            guard newPhase == .background else {
                return
            }

            //Exit app to test restoring app from a terminated state.
            Task {
                logger.info("Simulating app termination by exit(0)")

                exit(0)
            }
        }
    }
}
```

> Remember to remove this in production 😜.

**UIKit**   

```
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    private let logger = Logger(subsystem: "com.williamboles",
                                category: "appDelegate")

    // Omitted other methods

    func applicationDidEnterBackground(_ application: UIApplication) {
        //Exit app to test restoring app from a terminated state.
        Task {
            logger.info("Simulating app termination by exit(0)")

            exit(0)
        }
    }
}
```

> Remember to remove this in production 😜.

  
With the above changes, the app will be terminated when it goes into the background. However, once the app is terminated, the debugger will lose its link with the app, meaning any set breakpoints will be made invalid 😟. But don't be too sad, as we've built up this solution, we've been using `Logger` to output what the app has been doing. These logs will keep being produced even when the debugger isn't linked. These logs can be seen in the [Console](https://support.apple.com/en-gb/guide/console/welcome/mac?ref=williamboles.com) app. It is possible to filter the logs in the `Console` app - as logged events have been using `com.williamboles` as the `subsystem` value, this can be used to filter:

![Screenshot of Console app showing filtered events](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2025/04/Screenshot-of-console-showing-filtered-logs-min-1.jpg)

Running the app with the Console open should allow us to see that any in-progress downloads complete beyond the app termination.

So, we've implemented the ability to start downloading when the app is in the foreground and have that continue even if the app is terminated. However, there is one more common scenario to implement - handling duplicate requests. At the moment, if a duplicate download is made before the first one has completed, then the following error is thrown:

```
SWIFT TASK CONTINUATION MISUSE: download(from:to:) leaked its continuation!
```

This error is due to the metadata associated with the first download being replaced without the continuation being resumed.

There are two approaches we could implement to handle this scenario:

1. **Coalescing** \- piggybacking any subsequent request on the in-progress request and so calling multiple continuations when the download completes.
2. **Cancellation** \- cancelling the in-progress request, throwing an error and starting a new request.

Both of these approaches are valid, and which one you go for depends on the requirements of your app. **Cancellation** is more straightforward, so let's implement it.

To cancel a download, we need access to its `URLSessionDownloadTask` instance, so let's track the currently active downloads:

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

    // 1
    private var activeDownloads = [String: URLSessionDownloadTask]()

    // Omitted other methods

    func download(from fromURL: URL,
                  to toURL: URL) async throws -> URL {
        return try await withCheckedThrowingContinuation { continuation in
            // Omitted other statements

            let downloadTask = session.downloadTask(with: fromURL)
            // 2
            activeDownloads[fromURL.absoluteString] = downloadTask
            downloadTask.resume()
        }
    }

    private func cleanUpDownload(forURL url: URL) {
        //Omitted other statements

        // 3
        activeDownloads.removeValue(forKey: url.absoluteString)
    }
}
```

1. Similar to the continuation caching, the `URLSessionDownloadTask` instance is in a dictionary. The **remote URL** will act as the key.
2. The `URLSessionDownloadTask` instance is stored.
3. When a download is completed, the `URLSessionDownloadTask` instance is removed from `activeDownloads`.

Now that we have access to the `URLSessionDownloadTask` instance associated with a download, we can cancel it:

```
actor BackgroundDownloadService: NSObject {
    // Omitted properties and other methods

    func cancelDownload(forURL url: URL) {
        logger.info("Cancelling download for: \(url.absoluteString)")

        // 1
        inMemoryStore[url.absoluteString]?.resume(throwing: BackgroundDownloadError.cancelled)

        // 2
        activeDownloads[url.absoluteString]?.cancel()

        // 3
        cleanUpDownload(forURL: url)
    }
}
```

1. The continuation associated with `url` is resumed with a `cancelled` error being thrown.
2. The `URLSessionDownloadTask` instance associated with `url` is cancelled.
3. The metadata associated with `url` is deleted.

> The cancellation shown is a simple throw-everything-away cancellation; if you want a more sophisticated approach, check out [Don't Throw Anything Away with Pausable Downloads](https://williamboles.com/dont-throw-anything-away-with-pausable-downloads/) to see how a download can be resumed from where it was cancelled.

When a download cancels, the `downloadComplete(task:withError:)` method is called with a `URLError.cancelled` error. Let's update that method so we don't double-clean up cancelled downloads:

```
actor BackgroundDownloadService: NSObject {
   // Omitted properties and other methods

    private func downloadComplete(task: URLSessionTask,
                                  withError error: Error?) {
        guard let error = error else {
            return
        }

        // 1
        if let error = error as? URLError,
           error.code == .cancelled {
            return
        }

        // Omitted the rest of the method
    }
}
```

1. `error` is checked to see if it is an `URLError.cancelled` error; if so, the method is exited.

All that is left to do is to cancel the active download when a new download request is made:

```
actor BackgroundDownloadService: NSObject {
    // Omitted properties and other methods

    func download(from fromURL: URL,
                  to toURL: URL) async throws -> URL {
        // 1
        if activeDownloads[fromURL.absoluteString] != nil {
            cancelDownload(forURL: fromURL)
        }

        // Omitted the rest of the method
    }
}
```

1. `activeDownloads` is checked for an entry with `fromURL` as the key. If that entry exists, the download is cancelled; if that entry does not exist, cancellation is skipped.

With those changes, no continuation misuse error will be thrown when a duplicate download request is made.

Congratulations, that's all the code needed to support background-transfers.

### 💃 🕺

## 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 it isn't in the foreground. The enhanced user experience we gain from supporting background-transfers, I believe, outweighs any additional complexity added by supporting background-transfers.

To see the complete working example, visit the [repository](https://github.com/wibosco/BackgroundTransferRevised-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 downloaded images. `TheCatAPI` has an extensive library of freely available cat photos, which it shares via a JSON-based API. `TheCatAPI` does require you to [register](https://thecatapi.com/signup?ref=williamboles.com) to get full access to its API (limited access is provided without an API key). Once registered, you will be given an `x-api-key` token, which you can paste as the `APIKey` value in `NetworkService` to get full access.