Introduction

URLSession background sessions continue transfers when the app is suspended, but they require proper delegate implementation and app lifecycle handling. When the delegate is not connected at launch, when the configuration identifier is inconsistent, or when the app does not handle background completion events, transfers silently fail or never deliver their results. This is especially problematic for large file downloads, background uploads, and offline-first applications.

Symptoms

  • Background download starts but never completes
  • urlSession:downloadTask:didFinishDownloadingToURL: delegate never called
  • App does not receive completion events after being relaunched by the system
  • Background session configuration identifier mismatch
  • Downloaded file not found at expected location

Check background session events: ``swift // In AppDelegate or SceneDelegate func application( _ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void ) { print("Background session event: \(identifier)") // This must reconnect the session and store the completion handler }

Common Causes

  • URLSession delegate not set before background session events arrive
  • Background session configuration identifier changes between launches
  • App does not implement handleEventsForBackgroundURLSession
  • Downloaded file moved or deleted before app processes it
  • No Wi-Fi restriction on cellular network causing transfer pause

Step-by-Step Fix

  1. 1.Properly configure background session with persistent delegate:
  2. 2.```swift
  3. 3.class BackgroundDownloadManager: NSObject, ObservableObject {
  4. 4.static let shared = BackgroundDownloadManager()

private var session: URLSession! private var backgroundCompletionHandler: (() -> Void)? private let sessionIdentifier = "com.myapp.background-download"

override init() { super.init()

let config = URLSessionConfiguration.background( withIdentifier: sessionIdentifier ) config.isDiscretionary = true config.sessionSendsLaunchEvents = true config.waitsForConnectivity = true

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

func startDownload(url: URL) { let task = session.downloadTask(with: url) task.taskDescription = url.lastPathComponent task.resume() } } ```

  1. 1.Handle background session events in AppDelegate:
  2. 2.```swift
  3. 3.@main
  4. 4.class AppDelegate: UIResponder, UIApplicationDelegate {
  5. 5.var backgroundCompletionHandler: (() -> Void)?

func application( _ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void ) { // Store the completion handler backgroundCompletionHandler = completionHandler

// Ensure the download manager is initialized with its session _ = BackgroundDownloadManager.shared } } ```

  1. 1.**Implement URLSessionDownloadDelegate properly":
  2. 2.```swift
  3. 3.extension BackgroundDownloadManager: URLSessionDownloadDelegate {
  4. 4.func urlSession(
  5. 5._ session: URLSession,
  6. 6.downloadTask: URLSessionDownloadTask,
  7. 7.didFinishDownloadingTo location: URL
  8. 8.) {
  9. 9.// IMPORTANT: Move the file from the temporary location immediately
  10. 10.let destination = FileManager.default
  11. 11..urls(for: .documentDirectory, in: .userDomainMask)[0]
  12. 12..appendingPathComponent(downloadTask.taskDescription ?? "download")

do { if FileManager.default.fileExists(atPath: destination.path) { try FileManager.default.removeItem(at: destination) } try FileManager.default.moveItem(at: location, to: destination) print("Downloaded to: \(destination.path)") } catch { print("Failed to move file: \(error)") } }

func urlSession( _ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error? ) { if let error = error { print("Download failed: \(error.localizedDescription)") } else { print("Download completed successfully") }

// Signal that all background events are handled backgroundCompletionHandler?() backgroundCompletionHandler = nil }

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { // Called when all tasks in the background session are complete DispatchQueue.main.async { self.backgroundCompletionHandler?() self.backgroundCompletionHandler = nil } } } ```

  1. 1.**Handle download resumption after interruption":
  2. 2.```swift
  3. 3.func urlSession(
  4. 4._ session: URLSession,
  5. 5.task: URLSessionTask,
  6. 6.didCompleteWithError error: Error?
  7. 7.) {
  8. 8.if let error = error as? URLError,
  9. 9.error.code == .cancelled || error.code == .networkConnectionLost {
  10. 10.// Task was interrupted - check if resume data is available
  11. 11.if let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
  12. 12.// Save resume data for later
  13. 13.UserDefaults.standard.set(resumeData, forKey: "resume_\(task.taskIdentifier)")
  14. 14.print("Download interrupted, resume data saved")
  15. 15.}
  16. 16.}
  17. 17.}

// Resume a previously interrupted download func resumeDownload(taskIdentifier: Int) { guard let resumeData = UserDefaults.standard.data(forKey: "resume_\(taskIdentifier)") else { return }

let task = session.downloadTask(withResumeData: resumeData) task.resume() UserDefaults.standard.removeObject(forKey: "resume_\(taskIdentifier)") } ```

Prevention

  • Use a consistent session identifier across app launches
  • Always move downloaded files from temporary location immediately
  • Store background completion handler and call it when done
  • Test background downloads by suspending and killing the app
  • Set waitsForConnectivity = true for unreliable networks
  • Monitor URLSessionTask.countOfBytesReceived for progress updates