NSURLSession
background downloads
threading
iOS development
multitasking

NSURLSession Threads Tracking multiple background downloads

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Tracking multiple background downloads with NSURLSession is less about creating your own threads and more about understanding how Apple delivers delegate callbacks for long-running tasks. The reliable pattern is to use a background session, identify tasks by taskIdentifier, and keep your own state model so the app can reconnect to downloads even after suspension or relaunch.

Background Sessions and Callback Delivery

A background session lets the system continue downloads when your app is no longer in the foreground. That does not mean each download gets a dedicated thread you manage directly. Instead, the system invokes delegate methods on the queue you provide when creating the session.

In modern Swift, the equivalent setup looks like this:

swift
1import Foundation
2
3final class DownloadManager: NSObject, URLSessionDownloadDelegate {
4    private lazy var session: URLSession = {
5        let config = URLSessionConfiguration.background(withIdentifier: "com.example.downloads")
6        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
7    }()
8
9    func startDownload(from url: URL) {
10        let task = session.downloadTask(with: url)
11        task.resume()
12    }
13
14    func urlSession(_ session: URLSession,
15                    downloadTask: URLSessionDownloadTask,
16                    didFinishDownloadingTo location: URL) {
17        print("Finished task", downloadTask.taskIdentifier, "at", location.path)
18    }
19}

If you pass nil for the delegate queue, Apple creates a serial operation queue for delegate callbacks. That simplifies state management because your delegate methods are not running concurrently by default.

Tracking Multiple Downloads

The key to tracking more than one background task is to store task metadata in a dictionary keyed by taskIdentifier.

swift
1import Foundation
2
3struct DownloadItem {
4    let url: URL
5    var progress: Double
6}
7
8final class DownloadManager: NSObject, URLSessionDownloadDelegate {
9    private var items: [Int: DownloadItem] = [:]
10
11    private lazy var session: URLSession = {
12        let config = URLSessionConfiguration.background(withIdentifier: "com.example.downloads")
13        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
14    }()
15
16    func enqueue(_ url: URL) {
17        let task = session.downloadTask(with: url)
18        items[task.taskIdentifier] = DownloadItem(url: url, progress: 0)
19        task.resume()
20    }
21
22    func urlSession(_ session: URLSession,
23                    downloadTask: URLSessionDownloadTask,
24                    didWriteData bytesWritten: Int64,
25                    totalBytesWritten: Int64,
26                    totalBytesExpectedToWrite: Int64) {
27        guard totalBytesExpectedToWrite > 0 else { return }
28        let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
29        items[downloadTask.taskIdentifier]?.progress = progress
30        print("Task", downloadTask.taskIdentifier, "progress", progress)
31    }
32}

That mapping is far more important than trying to reason in terms of raw threads.

Reconnecting After App Relaunch

Background sessions survive beyond a single app run, so your app must be able to rebuild state. When iOS relaunches the app to deliver events, use the background session identifier and ask the session for its tasks if needed.

You also need to implement the application callback that stores the system completion handler until all events are finished.

swift
1import UIKit
2
3class AppDelegate: UIResponder, UIApplicationDelegate {
4    var backgroundCompletionHandler: (() -> Void)?
5
6    func application(_ application: UIApplication,
7                     handleEventsForBackgroundURLSession identifier: String,
8                     completionHandler: @escaping () -> Void) {
9        backgroundCompletionHandler = completionHandler
10    }
11}

Later, in the session delegate:

swift
1func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
2    DispatchQueue.main.async {
3        let appDelegate = UIApplication.shared.delegate as? AppDelegate
4        appDelegate?.backgroundCompletionHandler?()
5        appDelegate?.backgroundCompletionHandler = nil
6    }
7}

Without that handoff, the system does not know when your app has finished processing background events.

UI Updates and Threading

Delegate callbacks are not guaranteed to arrive on the main thread. If you need to update labels, progress bars, or collection views, dispatch back to the main queue explicitly.

swift
DispatchQueue.main.async {
    // update UI safely here
}

That separation matters even if the delegate queue is serial, because serial does not mean main-thread.

Common Pitfalls

The most common mistake is trying to track downloads by array position instead of taskIdentifier. Background tasks can finish in any order, and the app may be relaunched later, so stable identifiers matter.

Another mistake is updating UIKit directly from delegate callbacks. Even when things seem to work during testing, it is not a safe threading model.

A third issue is reusing the same background session identifier carelessly across unrelated workflows. A background session identifier should represent a consistent set of tasks the app knows how to restore.

Finally, many developers forget urlSessionDidFinishEvents. That omission often causes background download handling to feel incomplete or unreliable after the app wakes up.

Summary

  • 'NSURLSession background downloads are tracked through delegate callbacks, not custom thread management.'
  • Use a background session and map task state with taskIdentifier.
  • Persist enough metadata to reconnect to downloads after app suspension or relaunch.
  • Dispatch UI updates to the main queue explicitly.
  • Implement the background completion-handler flow so iOS can finish delivering events cleanly.

Course illustration
Course illustration

All Rights Reserved.