$npx -y skills add dpearson2699/swift-ios-skills --skill background-processingSchedule and execute background work on iOS using BGTaskScheduler. Use when registering BGAppRefreshTask for short background fetches, BGProcessingTask for long-running maintenance, BGContinuedProcessingTask (iOS 26+) for foreground-started work that continues in background, back
| 1 | # Background Processing |
| 2 | |
| 3 | Register, schedule, and execute background work on iOS using the BackgroundTasks |
| 4 | framework, background URLSession, and background push notifications. |
| 5 | |
| 6 | ## Contents |
| 7 | |
| 8 | - [Info.plist Configuration](#infoplist-configuration) |
| 9 | - [BGTaskScheduler Registration](#bgtaskscheduler-registration) |
| 10 | - [BGAppRefreshTask Patterns](#bgapprefreshtask-patterns) |
| 11 | - [BGProcessingTask Patterns](#bgprocessingtask-patterns) |
| 12 | - [BGContinuedProcessingTask (iOS 26+)](#bgcontinuedprocessingtask-ios-26) |
| 13 | - [Background URLSession Downloads](#background-urlsession-downloads) |
| 14 | - [Background Push Triggers](#background-push-triggers) |
| 15 | - [Common Mistakes](#common-mistakes) |
| 16 | - [Review Checklist](#review-checklist) |
| 17 | - [References](#references) |
| 18 | |
| 19 | ## Info.plist Configuration |
| 20 | |
| 21 | Every task identifier **must** be declared in `Info.plist` under |
| 22 | `BGTaskSchedulerPermittedIdentifiers`, or `submit(_:)` throws |
| 23 | `BGTaskScheduler.Error.Code.notPermitted`. |
| 24 | |
| 25 | ```xml |
| 26 | <key>BGTaskSchedulerPermittedIdentifiers</key> |
| 27 | <array> |
| 28 | <string>com.example.app.refresh</string> |
| 29 | <string>com.example.app.db-cleanup</string> |
| 30 | <string>com.example.app.export.*</string> |
| 31 | </array> |
| 32 | ``` |
| 33 | |
| 34 | Also enable the required `UIBackgroundModes`: |
| 35 | |
| 36 | ```xml |
| 37 | <key>UIBackgroundModes</key> |
| 38 | <array> |
| 39 | <string>fetch</string> <!-- Required for BGAppRefreshTask --> |
| 40 | <string>processing</string> <!-- Required for BGProcessingTask --> |
| 41 | </array> |
| 42 | ``` |
| 43 | |
| 44 | In Xcode: target > Signing & Capabilities > Background Modes > enable "Background fetch" and "Background processing". |
| 45 | |
| 46 | ## BGTaskScheduler Registration |
| 47 | |
| 48 | Register handlers **before** app launch completes. In UIKit, register in |
| 49 | `application(_:didFinishLaunchingWithOptions:)`; in SwiftUI, register in `App.init()`. |
| 50 | |
| 51 | ### UIKit Registration |
| 52 | |
| 53 | ```swift |
| 54 | import BackgroundTasks |
| 55 | |
| 56 | @main |
| 57 | class AppDelegate: UIResponder, UIApplicationDelegate { |
| 58 | func application( |
| 59 | _ application: UIApplication, |
| 60 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? |
| 61 | ) -> Bool { |
| 62 | BGTaskScheduler.shared.register( |
| 63 | forTaskWithIdentifier: "com.example.app.refresh", |
| 64 | using: nil // nil = default background queue |
| 65 | ) { task in |
| 66 | self.handleAppRefresh(task: task as! BGAppRefreshTask) |
| 67 | } |
| 68 | |
| 69 | BGTaskScheduler.shared.register( |
| 70 | forTaskWithIdentifier: "com.example.app.db-cleanup", |
| 71 | using: nil |
| 72 | ) { task in |
| 73 | self.handleDatabaseCleanup(task: task as! BGProcessingTask) |
| 74 | } |
| 75 | |
| 76 | return true |
| 77 | } |
| 78 | } |
| 79 | ``` |
| 80 | |
| 81 | ### SwiftUI Registration |
| 82 | |
| 83 | ```swift |
| 84 | import SwiftUI |
| 85 | import BackgroundTasks |
| 86 | |
| 87 | @main |
| 88 | struct MyApp: App { |
| 89 | init() { |
| 90 | BGTaskScheduler.shared.register( |
| 91 | forTaskWithIdentifier: "com.example.app.refresh", |
| 92 | using: nil |
| 93 | ) { task in |
| 94 | BackgroundTaskManager.shared.handleAppRefresh( |
| 95 | task: task as! BGAppRefreshTask |
| 96 | ) |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | var body: some Scene { |
| 101 | WindowGroup { ContentView() } |
| 102 | } |
| 103 | } |
| 104 | ``` |
| 105 | |
| 106 | ## BGAppRefreshTask Patterns |
| 107 | |
| 108 | Short-lived tasks (~30 seconds) for fetching small data updates. The system |
| 109 | decides when to launch; `earliestBeginDate` is only a lower-bound hint. |
| 110 | |
| 111 | ```swift |
| 112 | func scheduleAppRefresh() { |
| 113 | let request = BGAppRefreshTaskRequest( |
| 114 | identifier: "com.example.app.refresh" |
| 115 | ) |
| 116 | request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) |
| 117 | do { |
| 118 | try BGTaskScheduler.shared.submit(request) |
| 119 | } catch { |
| 120 | print("Could not schedule app refresh: \(error)") |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | func handleAppRefresh(task: BGAppRefreshTask) { |
| 125 | // Schedule the next refresh before doing work |
| 126 | scheduleAppRefresh() |
| 127 | |
| 128 | let fetchTask = Task { |
| 129 | do { |
| 130 | let data = try await APIClient.shared.fetchLatestFeed() |
| 131 | await FeedStore.shared.update(with: data) |
| 132 | task.setTaskCompleted(success: true) |
| 133 | } catch { |
| 134 | task.setTaskCompleted(success: false) |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | // CRITICAL: Handle expiration -- system can revoke time at any moment |
| 139 | task.expirationHandler = { |
| 140 | fetchTask.cancel() |
| 141 | task.setTaskCompleted(success: false) |
| 142 | } |
| 143 | } |
| 144 | ``` |
| 145 | |
| 146 | ## BGProcessingTask Patterns |
| 147 | |
| 148 | Long-running tasks (minutes) for maintenance, data processing, or cleanup. |
| 149 | They run while the device is idle and can require external power; the same |
| 150 | `earliestBeginDate` lower-bound rule applies. |
| 151 | |
| 152 | ```swift |
| 153 | func scheduleProcessingTask() { |
| 154 | let request = BGProcessingTaskRequest( |
| 155 | identifier: "com.example.app.db-cleanup" |
| 156 | ) |