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.Properly configure background session with persistent delegate:
- 2.```swift
- 3.class BackgroundDownloadManager: NSObject, ObservableObject {
- 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.Handle background session events in AppDelegate:
- 2.```swift
- 3.@main
- 4.class AppDelegate: UIResponder, UIApplicationDelegate {
- 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.**Implement URLSessionDownloadDelegate properly":
- 2.```swift
- 3.extension BackgroundDownloadManager: URLSessionDownloadDelegate {
- 4.func urlSession(
- 5._ session: URLSession,
- 6.downloadTask: URLSessionDownloadTask,
- 7.didFinishDownloadingTo location: URL
- 8.) {
- 9.// IMPORTANT: Move the file from the temporary location immediately
- 10.let destination = FileManager.default
- 11..urls(for: .documentDirectory, in: .userDomainMask)[0]
- 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.**Handle download resumption after interruption":
- 2.```swift
- 3.func urlSession(
- 4._ session: URLSession,
- 5.task: URLSessionTask,
- 6.didCompleteWithError error: Error?
- 7.) {
- 8.if let error = error as? URLError,
- 9.error.code == .cancelled || error.code == .networkConnectionLost {
- 10.// Task was interrupted - check if resume data is available
- 11.if let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
- 12.// Save resume data for later
- 13.UserDefaults.standard.set(resumeData, forKey: "resume_\(task.taskIdentifier)")
- 14.print("Download interrupted, resume data saved")
- 15.}
- 16.}
- 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 = truefor unreliable networks - Monitor
URLSessionTask.countOfBytesReceivedfor progress updates