
Background Processing
- 2.7k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
background-processing is a skill for iOS BGTaskScheduler tasks, background URLSession, and silent push handling.
About
background-processing covers registering, scheduling, and executing background work on iOS with BackgroundTasks, background URLSession, and silent push. Every task identifier must appear in Info.plist BGTaskSchedulerPermittedIdentifiers or submit throws notPermitted. UIBackgroundModes fetch and processing enable BGAppRefreshTask and BGProcessingTask respectively. Handlers register before app launch completes in AppDelegate or SwiftUI App.init. BGAppRefreshTask suits short fetches with earliestBeginDate hints and mandatory expirationHandler calling setTaskCompleted. BGProcessingTask supports longer maintenance when idle and optionally on external power. BGContinuedProcessingTask on iOS 26+ continues foreground-started exports with ProgressReporting and Live Activity updates. Background URLSession downloads use delegate callbacks and store completion handlers for relaunch. Silent push requires content-available 1 and apns-push-type background with priority 5. Common mistakes include missing plist identifiers, skipping setTaskCompleted, scheduling refreshes too frequently, and a review checklist for simulated launches and incremental cancellation-safe work.
- Info.plist BGTaskSchedulerPermittedIdentifiers required for every task.
- Register handlers before app launch; always call setTaskCompleted.
- expirationHandler must cancel work on BGAppRefresh and Processing tasks.
- BGContinuedProcessingTask on iOS 26+ with progress reporting.
- Background URLSession delegate and silent push apns-push-type background.
Background Processing by the numbers
- 2,721 all-time installs (skills.sh)
- +133 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #70 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
background-processing capabilities & compatibility
- Capabilities
- info.plist bgtaskschedulerpermittedidentifiers s · bgapprefreshtask and bgprocessingtask scheduling · bgcontinuedprocessingtask progress reporting ios · background urlsession delegate download handling · silent push content available handling · expiration handler and settaskcompleted enforcem
- Use cases
- api development · orchestration
- Platforms
- macOS
- IDEs
- vscode
- Pricing
- Free
What background-processing says it does
Every task identifier **must** be declared in `Info.plist`
CRITICAL: Handle expiration -- system can revoke time at any moment
earliestBeginDate is a lower-bound hint
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill background-processingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.7k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
How do I register, schedule, and complete iOS background refresh and processing tasks correctly?
Register and schedule iOS background tasks with BGTaskScheduler, URLSession downloads, and silent push triggers.
Who is it for?
iOS developers implementing BGAppRefreshTask, BGProcessingTask, or background downloads.
Skip if: Skip for Android background work or foreground-only SwiftUI UI tasks.
When should I use this skill?
User configures BGTaskScheduler, Info.plist background modes, URLSession background downloads, or silent push.
What you get
Permitted identifiers, registered handlers, expiration-safe tasks, and validated background modes.
- BGTaskScheduler registration code
- Info.plist background configuration
- Expiration and completion handler patterns
By the numbers
- Covers 3 BGTaskScheduler task types: BGAppRefreshTask, BGProcessingTask, and BGContinuedProcessingTask (iOS 26+)
Files
Background Processing
Register, schedule, and execute background work on iOS using the BackgroundTasks framework, background URLSession, and background push notifications.
Contents
- Info.plist Configuration
- BGTaskScheduler Registration
- BGAppRefreshTask Patterns
- BGProcessingTask Patterns
- BGContinuedProcessingTask (iOS 26+)
- Background URLSession Downloads
- Background Push Triggers
- Common Mistakes
- Review Checklist
- References
Info.plist Configuration
Every task identifier must be declared in Info.plist under BGTaskSchedulerPermittedIdentifiers, or submit(_:) throws BGTaskScheduler.Error.Code.notPermitted.
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.example.app.refresh</string>
<string>com.example.app.db-cleanup</string>
<string>com.example.app.export.*</string>
</array>Also enable the required UIBackgroundModes:
<key>UIBackgroundModes</key>
<array>
<string>fetch</string> <!-- Required for BGAppRefreshTask -->
<string>processing</string> <!-- Required for BGProcessingTask -->
</array>In Xcode: target > Signing & Capabilities > Background Modes > enable "Background fetch" and "Background processing".
BGTaskScheduler Registration
Register handlers before app launch completes. In UIKit, register in application(_:didFinishLaunchingWithOptions:); in SwiftUI, register in App.init().
UIKit Registration
import BackgroundTasks
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.refresh",
using: nil // nil = default background queue
) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.db-cleanup",
using: nil
) { task in
self.handleDatabaseCleanup(task: task as! BGProcessingTask)
}
return true
}
}SwiftUI Registration
import SwiftUI
import BackgroundTasks
@main
struct MyApp: App {
init() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.refresh",
using: nil
) { task in
BackgroundTaskManager.shared.handleAppRefresh(
task: task as! BGAppRefreshTask
)
}
}
var body: some Scene {
WindowGroup { ContentView() }
}
}BGAppRefreshTask Patterns
Short-lived tasks (~30 seconds) for fetching small data updates. The system decides when to launch based on usage patterns. Review notes should say earliestBeginDate is a lower-bound hint and the system may run the task later.
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(
identifier: "com.example.app.refresh"
)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
// earliestBeginDate is a lower-bound hint; the system may delay launch.
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}
func handleAppRefresh(task: BGAppRefreshTask) {
// Schedule the next refresh before doing work
scheduleAppRefresh()
let fetchTask = Task {
do {
let data = try await APIClient.shared.fetchLatestFeed()
await FeedStore.shared.update(with: data)
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
// CRITICAL: Handle expiration -- system can revoke time at any moment
task.expirationHandler = {
fetchTask.cancel()
task.setTaskCompleted(success: false)
}
}BGProcessingTask Patterns
Long-running tasks (minutes) for maintenance, data processing, or cleanup. Runs only when device is idle and (optionally) charging. Review notes should say earliestBeginDate is a lower-bound hint and the system may run the task later.
func scheduleProcessingTask() {
let request = BGProcessingTaskRequest(
identifier: "com.example.app.db-cleanup"
)
request.requiresNetworkConnectivity = false
request.requiresExternalPower = true
request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60)
// earliestBeginDate is a lower-bound hint; the system may delay launch.
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule processing task: \(error)")
}
}
func handleDatabaseCleanup(task: BGProcessingTask) {
scheduleProcessingTask()
let cleanupTask = Task {
do {
try await DatabaseManager.shared.purgeExpiredRecords()
try await DatabaseManager.shared.rebuildIndexes()
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
task.expirationHandler = {
cleanupTask.cancel()
task.setTaskCompleted(success: false)
}
}BGContinuedProcessingTask (iOS 26+)
A task initiated in the foreground by a user action that continues running in the background. The system displays progress via a Live Activity. Conforms to ProgressReporting.
Availability: iOS 26.0+, iPadOS 26.0+
Unlike BGAppRefreshTask and BGProcessingTask, this task starts immediately from the foreground. The system can terminate it under resource pressure, prioritizing tasks that report minimal progress first. Set expirationHandler for user or system cancellation, cancel in-flight work, and clean up partial output before reporting completion.
import BackgroundTasks
func startExport() {
// Register the task handler at app launch, not here.
// BGTaskScheduler requires registration before app launch completes.
let jobID = UUID().uuidString
let request = BGContinuedProcessingTaskRequest(
identifier: "com.example.app.export.\(jobID)",
title: "Exporting Photos",
subtitle: "Processing 247 items"
)
// Use a permitted base wildcard identifier: com.example.app.export.*
// earliestBeginDate is ignored for continued processing requests.
// .queue: begin as soon as possible if can't run immediately
// .fail: fail submission if can't run immediately
request.strategy = .queue
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not submit continued processing task: \(error)")
}
}
func performExport(task: BGContinuedProcessingTask) async {
let items = await PhotoLibrary.shared.itemsToExport()
let progress = task.progress
progress.totalUnitCount = Int64(items.count)
for (index, item) in items.enumerated() {
if Task.isCancelled { break }
await PhotoExporter.shared.export(item)
progress.completedUnitCount = Int64(index + 1)
// Update the user-facing title/subtitle
task.updateTitle(
"Exporting Photos",
subtitle: "\(index + 1) of \(items.count) complete"
)
}
task.setTaskCompleted(success: !Task.isCancelled)
}For GPU work, check support and enable Background GPU Access (com.apple.developer.background-tasks.continued-processing.gpu):
let supported = BGTaskScheduler.supportedResources
if supported.contains(.gpu) {
request.requiredResources = .gpu
}Background URLSession Downloads
Use URLSessionConfiguration.background for downloads that continue even after the app is suspended or terminated. The system handles the transfer out of process.
class DownloadManager: NSObject, URLSessionDownloadDelegate {
static let shared = DownloadManager()
private lazy var session: URLSession = {
let config = URLSessionConfiguration.background(
withIdentifier: "com.example.app.background-download"
)
config.isDiscretionary = true
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
func startDownload(from url: URL) {
let task = session.downloadTask(with: url)
task.earliestBeginDate = Date(timeIntervalSinceNow: 60)
task.resume()
}
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL
) {
// Move file from tmp before this method returns
let dest = FileManager.default.urls(
for: .documentDirectory, in: .userDomainMask
)[0].appendingPathComponent("download.dat")
try? FileManager.default.moveItem(at: location, to: dest)
}
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: (any Error)?
) {
if let error { print("Download failed: \(error)") }
}
}Handle app relaunch — store and invoke the system completion handler:
// In AppDelegate:
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
backgroundSessionCompletionHandler = completionHandler
}
// In URLSessionDelegate — call stored handler when events finish:
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
Task { @MainActor in
self.backgroundSessionCompletionHandler?()
self.backgroundSessionCompletionHandler = nil
}
}Background Push Triggers
Silent push notifications wake your app briefly to fetch new content. Set content-available: 1 in the push payload.
{ "aps": { "content-available": 1 }, "custom-data": "new-messages" }Send the APNs request with apns-push-type: background and apns-priority: 5. Background push delivery is low priority and not guaranteed; keep sends infrequent, generally no more than two or three per hour.
Handle in AppDelegate:
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler:
@escaping (UIBackgroundFetchResult) -> Void
) {
Task {
do {
let hasNew = try await MessageStore.shared.fetchNewMessages()
completionHandler(hasNew ? .newData : .noData)
} catch {
completionHandler(.failed)
}
}
}Enable "Remote notifications" in Background Modes and register:
UIApplication.shared.registerForRemoteNotifications()Common Mistakes
1. Missing Info.plist identifiers
// DON'T: Submit a task whose identifier isn't in BGTaskSchedulerPermittedIdentifiers
let request = BGAppRefreshTaskRequest(identifier: "com.example.app.refresh")
try BGTaskScheduler.shared.submit(request) // Throws .notPermitted
// DO: Add every identifier to Info.plist BGTaskSchedulerPermittedIdentifiers
// <string>com.example.app.refresh</string>2. Not calling setTaskCompleted(success:)
// DON'T: Return without marking completion -- system penalizes future scheduling
func handleRefresh(task: BGAppRefreshTask) {
Task {
let data = try await fetchData()
await store.update(data)
// Missing: task.setTaskCompleted(success:)
}
}
// DO: Always call setTaskCompleted on every code path
func handleRefresh(task: BGAppRefreshTask) {
let work = Task {
do {
let data = try await fetchData()
await store.update(data)
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
task.expirationHandler = {
work.cancel()
task.setTaskCompleted(success: false)
}
}3. Ignoring the expiration handler
// DON'T: Assume your task will run to completion
func handleCleanup(task: BGProcessingTask) {
Task { await heavyWork() }
// No expirationHandler -- system terminates ungracefully
}
// DO: Set expirationHandler to cancel work and mark completed
func handleCleanup(task: BGProcessingTask) {
let work = Task { await heavyWork() }
task.expirationHandler = {
work.cancel()
task.setTaskCompleted(success: false)
}
}4. Scheduling too frequently
// DON'T: Request refresh every minute -- system throttles aggressively
request.earliestBeginDate = Date(timeIntervalSinceNow: 60)
// DO: Use reasonable intervals (15+ minutes for refresh)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
// earliestBeginDate is a hint -- the system chooses actual launch time5. Over-relying on background time
// DON'T: Start a 10-minute operation assuming it will finish
func handleRefresh(task: BGAppRefreshTask) {
Task { await tenMinuteSync() }
}
// DO: Design work to be incremental and cancellable
func handleRefresh(task: BGAppRefreshTask) {
let work = Task {
for batch in batches {
try Task.checkCancellation()
await processBatch(batch)
await saveBatchProgress(batch)
}
task.setTaskCompleted(success: true)
}
task.expirationHandler = {
work.cancel()
task.setTaskCompleted(success: false)
}
}Review Checklist
- [ ] All task identifiers listed in
BGTaskSchedulerPermittedIdentifiers - [ ] Required
UIBackgroundModesenabled (fetch,processing) - [ ] Tasks registered before app launch completes
- [ ]
setTaskCompleted(success:)called on every code path - [ ]
expirationHandlerset and cancels in-flight work - [ ] Next task scheduled inside the handler (re-schedule pattern)
- [ ]
earliestBeginDateuses reasonable intervals and is treated as a hint - [ ] Background URLSession uses delegate (not async/closures)
- [ ] Background URLSession file moved in
didFinishDownloadingTobefore return - [ ]
handleEventsForBackgroundURLSessionstores and calls completion handler - [ ] Background push payload includes
content-available: 1 - [ ] Background push APNs request uses
apns-push-type: backgroundandapns-priority: 5 - [ ]
fetchCompletionHandlercalled promptly with correct result - [ ] BGContinuedProcessingTask reports progress via
ProgressReporting - [ ] Work is incremental and cancellation-safe (
Task.checkCancellation()) - [ ] No blocking synchronous work in task handlers
References
- See references/background-task-patterns.md for extended patterns, background
URLSession edge cases, debugging with simulated launches, and background push best practices.
{
"skill_name": "background-processing",
"evals": [
{
"id": 1,
"name": "bg-task-registration-review",
"prompt": "Review this iOS background task setup for a SwiftUI app. It registers a BGAppRefreshTask from a view's .task modifier, submits a BGProcessingTask without adding BGTaskSchedulerPermittedIdentifiers, and does not set an expirationHandler. Give a concise fix plan and corrected Swift/Info.plist snippets.",
"expected_output": "Identifies launch-time registration, Info.plist permitted identifiers, required UIBackgroundModes, expiration handling, setTaskCompleted on all paths, and rescheduling patterns.",
"files": [],
"assertions": [
"Says BGTaskScheduler handlers must be registered before app launch completes, not from a view task.",
"Names BGTaskSchedulerPermittedIdentifiers and the fetch/processing UIBackgroundModes requirements.",
"Requires expirationHandler cancellation and setTaskCompleted(success:) on every path.",
"Shows or describes scheduling the next request from inside the task handler."
]
},
{
"id": 2,
"name": "continued-processing-ios26",
"prompt": "I need an iOS 26 photo export that starts from a user tap and keeps running if the user backgrounds the app. It may use GPU acceleration. What BackgroundTasks API should I use, and what are the identifier, progress, cancellation, and entitlement gotchas?",
"expected_output": "Recommends BGContinuedProcessingTaskRequest/BGContinuedProcessingTask and covers foreground user action, wildcard permitted identifier base with unique job identifiers, ProgressReporting, updateTitle/subtitle, expiration cancellation, supportedResources, and the Background GPU Access entitlement.",
"files": [],
"assertions": [
"Recommends BGContinuedProcessingTaskRequest and BGContinuedProcessingTask for iOS 26 foreground-started work.",
"Explains that submitted identifiers should be unique per job under a bundle-prefixed wildcard permitted identifier.",
"Mentions ProgressReporting or task.progress and cancellation/expiration handling.",
"Mentions checking BGTaskScheduler.supportedResources before requesting GPU and requiring the Background GPU Access entitlement."
]
},
{
"id": 3,
"name": "background-push-boundary",
"prompt": "Our server wants to wake the app with silent pushes every few minutes to keep chat data fresh, and one engineer suggested using apns-priority 10 so they arrive instantly. Review the plan and say what should change. Do not redesign our notification UI.",
"expected_output": "Keeps scope to background push refresh behavior, rejects frequent silent pushes and priority 10, requires content-available, remote-notification mode, apns-push-type background, apns-priority 5, and prompt fetch completion.",
"files": [],
"assertions": [
"Requires content-available: 1 and the Remote notifications background mode.",
"Requires APNs headers apns-push-type: background and apns-priority: 5.",
"Warns that background pushes are low priority, throttled, not guaranteed, and should not be sent every few minutes.",
"Stays focused on background refresh behavior rather than redesigning visible notification UI."
]
}
]
}
Background Task Patterns — Extended Reference
Overflow reference for the background-processing skill. Contains debugging tips, advanced background URLSession patterns, background push best practices, and SwiftUI integration patterns.
Contents
- Debugging Background Tasks
- Advanced BGProcessingTask Patterns
- Background URLSession — Extended Patterns
- Background Push — Extended Patterns
- SwiftUI BackgroundTask Modifier
- BGContinuedProcessingTask — Extended Patterns
Debugging Background Tasks
Simulating Task Launches in Xcode
Use the LLDB console to trigger tasks instantly during development. The app must be running on a device in the debugger with a breakpoint hit or paused.
These are Apple-documented private functions for development only. Do not include references to them in App Store-submitted code.
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.app.refresh"]For processing tasks:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.app.db-cleanup"]To simulate early termination (expiration):
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.example.app.refresh"]Verifying Pending Tasks
Check what tasks are currently scheduled:
BGTaskScheduler.shared.getPendingTaskRequests { requests in
for request in requests {
print("Pending: \(request.identifier), earliest: \(String(describing: request.earliestBeginDate))")
}
}Common Debugging Issues
| Symptom | Cause | Fix |
|---|---|---|
| Task never fires | Identifier not in Info.plist | Add to BGTaskSchedulerPermittedIdentifiers |
| Task never fires | Background modes not enabled | Enable fetch and/or processing in capabilities |
| Task never fires on device | Background App Refresh disabled or Low Power Mode active | Check UIApplication.shared.backgroundRefreshStatus; Low Power Mode reduces background runtime |
.notPermitted error | Identifier mismatch | Verify exact string match between code and plist |
.unavailable error | Running in extension | BGTaskScheduler not available in app extensions |
.tooManyPendingTaskRequests | More than 1 refresh task or 10 processing tasks scheduled in total | Cancel old requests before submitting new ones |
Submitting an unexecuted task request with the same identifier replaces the previous request.
Advanced BGProcessingTask Patterns
Conditional Requirements
Use requiresExternalPower and requiresNetworkConnectivity to ensure the system only launches your task when conditions are met:
func scheduleSyncTask() {
let request = BGProcessingTaskRequest(
identifier: "com.example.app.full-sync"
)
// Only run when charging and connected to network
request.requiresExternalPower = true
request.requiresNetworkConnectivity = true
// Don't run before 2 AM
var components = DateComponents()
components.hour = 2
if let twoAM = Calendar.current.nextDate(
after: Date(),
matching: components,
matchingPolicy: .nextTime
) {
request.earliestBeginDate = twoAM
}
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Failed to schedule sync: \(error)")
}
}Incremental Work with Checkpointing
Design tasks to save progress so they can resume if terminated:
func handleMigration(task: BGProcessingTask) {
let work = Task {
let lastProcessed = UserDefaults.standard.integer(
forKey: "migrationLastIndex"
)
let items = try await loadItems()
for (index, item) in items.dropFirst(lastProcessed).enumerated() {
try Task.checkCancellation()
try await migrate(item)
// Checkpoint progress
UserDefaults.standard.set(
lastProcessed + index + 1,
forKey: "migrationLastIndex"
)
}
task.setTaskCompleted(success: true)
}
task.expirationHandler = {
work.cancel()
// Progress is saved -- next launch picks up where we left off
task.setTaskCompleted(success: false)
}
}Background URLSession — Extended Patterns
Configuration Best Practices
let config = URLSessionConfiguration.background(
withIdentifier: "com.example.app.background-transfer"
)
// isDiscretionary = true: system picks optimal time (WiFi, power)
// Use for non-urgent transfers
config.isDiscretionary = true
// sessionSendsLaunchEvents = true: app relaunched when transfer completes
config.sessionSendsLaunchEvents = true
// Set reasonable timeouts
config.timeoutIntervalForResource = 60 * 60 * 24 * 7 // 7 days
// Allow cellular (default is true)
config.allowsCellularAccess = trueUpload with Background Session
func uploadFile(at fileURL: URL) {
var request = URLRequest(url: URL(string: "https://api.example.com/upload")!)
request.httpMethod = "POST"
let uploadTask = session.uploadTask(with: request, fromFile: fileURL)
uploadTask.resume()
}Important: Background sessions only support uploadTask(with:fromFile:) and downloadTask(with:). Data tasks, uploadTask(with:from:) (Data), and closure/async-based tasks are not supported.
Handling App Relaunch
When the system completes a background transfer and your app is not running, it relaunches the app. You must:
1. Store the completion handler from application(_:handleEventsForBackgroundURLSession:completionHandler:) 2. Recreate the URLSession with the same identifier 3. Call the stored completion handler in urlSessionDidFinishEvents
// AppDelegate
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
// Recreating session with the same identifier reconnects to the transfer
_ = DownloadManager.shared.session // trigger lazy init
DownloadManager.shared.completionHandler = completionHandler
}
// In your URLSessionDelegate
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
Task { @MainActor in
self.completionHandler?()
self.completionHandler = nil
}
}Handling Download Errors and Retries
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: (any Error)?
) {
guard let error else { return } // Success handled in didFinishDownloadingTo
let nsError = error as NSError
// Check if download can be resumed
if let resumeData = nsError.userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
// Store resumeData and retry later
let downloadTask = session.downloadTask(withResumeData: resumeData)
downloadTask.resume()
return
}
// Non-resumable error -- retry from scratch or notify user
if nsError.code == NSURLErrorNetworkConnectionLost {
// Re-enqueue the download
if let url = task.originalRequest?.url {
let newTask = session.downloadTask(with: url)
newTask.resume()
}
}
}Progress Tracking
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64
) {
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
// Update UI on main actor if app is in foreground
Task { @MainActor in
DownloadProgressStore.shared.update(
taskID: downloadTask.taskIdentifier,
progress: progress
)
}
}Background Push — Extended Patterns
Push Payload Requirements
The content-available: 1 flag is required. You can include custom data:
{
"aps": {
"content-available": 1
},
"type": "new-message",
"conversation-id": "abc-123"
}Do not include alert, badge, or sound if you only want a silent push. Including visual notification keys changes the push behavior.
Send background notification requests with APNs headers:
apns-push-type: background
apns-priority: 5Rate Limiting
Apple throttles background push delivery. Guidelines:
- Delivery is low priority and not guaranteed.
- Do not send more than two or three background notifications per hour.
- The system may hold only the newest background notification and discard older
held notifications.
- If the user force-quits the app, background pushes stop until the next manual
launch.
- Use
apns-priority: 5; high-priority pushes are for user-visible
notifications, not silent refresh.
Handling Push with Async Work
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler:
@escaping (UIBackgroundFetchResult) -> Void
) {
guard let type = userInfo["type"] as? String else {
completionHandler(.noData)
return
}
Task {
do {
switch type {
case "new-message":
let conversationID = userInfo["conversation-id"] as? String
let fetched = try await MessageService.shared
.fetchMessages(for: conversationID)
completionHandler(fetched ? .newData : .noData)
case "config-update":
try await ConfigService.shared.refreshConfig()
completionHandler(.newData)
default:
completionHandler(.noData)
}
} catch {
completionHandler(.failed)
}
}
}Important: You have approximately 30 seconds to call completionHandler. Failure to do so causes the system to penalize your app's background push budget.
SwiftUI BackgroundTask Modifier
SwiftUI provides a .backgroundTask modifier as an alternative to manual BGTaskScheduler registration:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup { ContentView() }
.backgroundTask(.appRefresh("com.example.app.refresh")) {
await refreshFeed()
// Schedule the next one
scheduleAppRefresh()
}
}
}This is a SwiftUI handler for matching background tasks. You still need the Info.plist identifiers, background modes, scheduling, and cancellation-safe work patterns.
BGContinuedProcessingTask — Extended Patterns
Checking Supported Resources
Before requesting GPU or other resources, verify the device supports them and enable Background GPU Access (com.apple.developer.background-tasks.continued-processing.gpu) for GPU work:
let supported = BGTaskScheduler.supportedResources
if supported.contains(.gpu) {
request.requiredResources = .gpu
}Submission Strategies
BGContinuedProcessingTaskRequest.SubmissionStrategy controls behavior when the system cannot run the task immediately:
| Strategy | Behavior |
|---|---|
.queue | Task is queued and starts as soon as possible |
.fail | Submission fails immediately if can't run now |
Use .fail when the work is only relevant in the current moment (e.g., a user is waiting). Use .queue for work that can start whenever the system allows.
Cancellation by the User
The system shows a Live Activity for continued processing tasks. The user can cancel the task from there. Handle this in your expiration handler:
task.expirationHandler = {
// Clean up partial work
cleanupPartialExport()
task.setTaskCompleted(success: false)
}Progress Reporting
The system uses your Progress object to decide termination priority. Tasks with no progress updates are terminated first under resource pressure:
// Report fine-grained progress
let progress = task.progress
progress.totalUnitCount = Int64(totalItems)
for (index, item) in items.enumerated() {
try Task.checkCancellation()
await process(item)
progress.completedUnitCount = Int64(index + 1)
}Related skills
How it compares
Use background-processing for BGTaskScheduler and plist-compliant setup; use push notification skills alone when you only need foreground APNs handling.
FAQ
Why does submit throw notPermitted?
The task identifier is missing from BGTaskSchedulerPermittedIdentifiers in Info.plist.
When must setTaskCompleted be called?
On every code path including expirationHandler; missing completion penalizes future scheduling.
What is earliestBeginDate?
A lower-bound hint; the system may delay launch beyond that date.
Is Background Processing safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.