Community-maintained Agent Skills for complete Swift and Apple-platform app delivery.
72
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Medium
Suggest reviewing before use
Use this reference when configuring out-of-process background download and upload tasks, handling app relaunch events, and managing download progress in iOS.
Background sessions allow HTTP/HTTPS upload and download transfers to continue when the app is suspended or terminated by the system. The system manages the transfer in a separate process and wakes the app on completion.
If the user force-quits the app from the multitasking screen, iOS cancels the background transfers and does not relaunch the app until the user opens it again.
@available(iOS 15.0, *)
final class BackgroundDownloadManager: NSObject, Sendable {
static let shared = BackgroundDownloadManager()
/// Use a unique identifier tied to your app's bundle ID.
/// The system uses this to reconnect to the session after relaunch.
private let sessionID = "com.example.app.background-downloads"
/// Lazy-initialized background session. Must use a delegate, not async/await,
/// because the system delivers events through the delegate after app relaunch.
lazy var session: URLSession = {
let config = URLSessionConfiguration.background(
withIdentifier: sessionID
)
config.isDiscretionary = false // Start immediately (true = system-scheduled)
config.sessionSendsLaunchEvents = true // Wake app on completion
config.allowsExpensiveNetworkAccess = true
config.allowsConstrainedNetworkAccess = false // Respect Low Data Mode
config.timeoutIntervalForResource = 24 * 60 * 60 // 24 hours
return URLSession(
configuration: config,
delegate: self,
delegateQueue: nil // Use a system-managed serial queue
)
}()
/// Store completionHandler from AppDelegate for system callback
nonisolated(unsafe) var backgroundCompletionHandler: (() -> Void)?
}| Property | Effect |
|---|---|
isDiscretionary | true = system schedules for optimal battery/network. Use for non-urgent sync. false = start immediately. |
sessionSendsLaunchEvents | Relaunches the app when transfers complete. Required for completion handling. |
allowsConstrainedNetworkAccess | false = honor Low Data Mode. Good for optional downloads. |
allowsExpensiveNetworkAccess | false = Wi-Fi only. Use for large transfers. |
timeoutIntervalForResource | Maximum time for the entire transfer. Default is 7 days. |
Background downloads must use downloadTask(with:), not data(for:).
The async/await overloads are not supported for background sessions --
you must use the delegate pattern.
extension BackgroundDownloadManager {
func startDownload(from url: URL) -> URLSessionDownloadTask {
let task = session.downloadTask(with: url)
task.earliestBeginDate = Date() // Start now
task.countOfBytesClientExpectsToSend = 0
task.countOfBytesClientExpectsToReceive = 50 * 1024 * 1024 // Estimated size
task.resume()
return task
}
func startDownload(from url: URL, resumeData: Data) -> URLSessionDownloadTask {
let task = session.downloadTask(withResumeData: resumeData)
task.resume()
return task
}
}extension BackgroundDownloadManager: URLSessionDownloadDelegate {
nonisolated func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL
) {
// CRITICAL: Move or open the file before this method returns.
// The temporary file is only available until the delegate returns.
let destinationDir = FileManager.default.urls(
for: .documentDirectory,
in: .userDomainMask
).first!
let filename = downloadTask.originalRequest?.url?.lastPathComponent ?? UUID().uuidString
let destination = destinationDir.appendingPathComponent(filename)
do {
// Remove existing file if present
if FileManager.default.fileExists(atPath: destination.path) {
try FileManager.default.removeItem(at: destination)
}
try FileManager.default.moveItem(at: location, to: destination)
// Notify the app (post notification, update state, etc.)
} catch {
// Handle file move failure
}
}
nonisolated func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64
) {
guard totalBytesExpectedToWrite > 0 else { return }
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
// Update progress UI (dispatch to main if needed)
}
nonisolated func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: (any Error)?
) {
guard let error else { return } // Success handled in didFinishDownloadingTo
// Check for resume data on failure
let nsError = error as NSError
if let resumeData = nsError.userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
// Store resumeData for retry
saveResumeData(resumeData, for: task)
}
}
private func saveResumeData(_ data: Data, for task: URLSessionTask) {
// Persist resume data to disk for later retry
let key = task.originalRequest?.url?.absoluteString ?? ""
let path = FileManager.default.temporaryDirectory
.appendingPathComponent("resume-\(key.hashValue)")
try? data.write(to: path)
}
}When the system completes a background transfer and the app is not
running, it relaunches the app and calls the AppDelegate method. If you use
the completion-handler overload, call the system's completion handler after
processing all events.
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
// Store the completion handler. The BackgroundDownloadManager will
// call it after processing all pending events.
BackgroundDownloadManager.shared.backgroundCompletionHandler = completionHandler
// Accessing .session triggers lazy initialization, which reconnects
// to the background session and starts delivering delegate events.
_ = BackgroundDownloadManager.shared.session
}
}extension BackgroundDownloadManager: URLSessionDelegate {
nonisolated func urlSessionDidFinishEvents(
forBackgroundURLSession session: URLSession
) {
// Called after ALL pending delegate events have been delivered.
// Call the stored completion handler on the main thread.
Task { @MainActor in
backgroundCompletionHandler?()
backgroundCompletionHandler = nil
}
}
}@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Important: For the handler-based UIApplicationDelegate overload, the
completion handler must be called exactly once and on the main thread. Failing
to call it causes the system to take a snapshot of the app in the wrong state
and may waste background runtime.
Background uploads require data from a file, not from memory.
extension BackgroundDownloadManager {
func startUpload(
to url: URL,
fileURL: URL,
method: String = "POST",
headers: [String: String] = [:]
) -> URLSessionUploadTask {
var request = URLRequest(url: url)
request.httpMethod = method
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
let task = session.uploadTask(with: request, fromFile: fileURL)
task.resume()
return task
}
}extension BackgroundDownloadManager {
nonisolated func urlSession(
_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64
) {
guard totalBytesExpectedToSend > 0 else { return }
let progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
// Update progress UI
}
}Constraints of background uploads:
uploadTask(with:fromFile:)).uploadTask(with:from: Data) is not supported in background sessions.If two sessions share the same identifier, events may be delivered to the wrong delegate. Use your bundle identifier as a prefix.
The data(for:) and download(for:) async methods are not available
on background sessions. Use downloadTask(with:) and the delegate.
Data tasks (dataTask) are not supported in background sessions. Convert
data requests to download tasks if needed for background execution.
WebSocket tasks are not background transfer tasks; reconnect them when the app
is active again.
Store any state you need (task identifiers, file destinations) to disk. Do not rely on in-memory state surviving a background relaunch. User force-quit is different from system termination: iOS cancels outstanding background transfers and will not relaunch the app automatically.
The temporary file at location is available until the delegate method
returns. Move it to preserve it, or open it for reading before returning.
Store the completion handler from
application(_:handleEventsForBackgroundURLSession:completionHandler:)
and invoke it in urlSessionDidFinishEvents(forBackgroundURLSession:)
on the main thread.
Background session behavior differs significantly between the Simulator and real devices. Always test background transfers on hardware.
Bridge the delegate-based background download to an @Observable model
for live UI updates.
@MainActor
@Observable final class DownloadTracker {
var downloads: [URL: DownloadProgress] = [:]
struct DownloadProgress: Sendable {
var fractionCompleted: Double = 0
var state: State = .downloading
enum State: Sendable { case downloading, completed, failed }
}
func updateProgress(for url: URL, fraction: Double) {
downloads[url, default: DownloadProgress()].fractionCompleted = fraction
}
func markCompleted(for url: URL) {
downloads[url]?.state = .completed
downloads[url]?.fractionCompleted = 1.0
}
func markFailed(for url: URL) {
downloads[url]?.state = .failed
}
}Wire the delegate to the tracker:
extension BackgroundDownloadManager {
// Called from delegate methods; dispatches to MainActor
func reportProgress(for url: URL, fraction: Double) {
Task { @MainActor in
downloadTracker.updateProgress(for: url, fraction: fraction)
}
}
}.tessl-plugin
skills
accessorysetupkit
references
activitykit
adattributionkit
references
alarmkit
references
app-clips
app-intents
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
audioaccessorykit
references
authentication
references
avfoundation-audio
references
avkit
background-processing
references
browserenginekit
callkit
references
carplay
cloudkit
contacts-framework
references
core-bluetooth
references
core-data
core-haptics
references
core-motion
references
core-nfc
references
coreml
references
cryptokit
cryptotokenkit
debugging-instruments
device-integrity
references
energykit
references
eventkit
family-controls
references
financekit
focus-engine
gamekit
git-branching-workflow
references
healthkit
references
homekit
references
ios-accessibility
ios-app-workflow
references
ios-ettrace-performance
ios-localization
ios-memgraph-analysis
ios-networking
ios-simulator
references
metrickit
references
musickit
references
natural-language
paperkit
references
passkit
references
pdfkit
pencilkit
references
permissionkit
references
photokit
push-notifications
quicklook
references
realitykit
references
relevancekit
references
scenekit
sensorkit
shazamkit
references
speech-recognition
references
spritekit
storekit
swift-api-design-guidelines
swift-architecture
references
swift-charts
swift-codable
references
swift-code-review
swift-concurrency
swift-formatstyle
references
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-responsive-layout
swiftui-uikit-interop
swiftui-webkit
tabletopkit
tipkit
vision-framework
weatherkit
references