background-processing
dpearson2699/swift-ios-skills
Schedule and execute background work on iOS using BGTaskScheduler, background URLSession, and push notifications.
What is background-processing?
Implement background processing on iOS using the BackgroundTasks framework to handle short fetches (BGAppRefreshTask), long-running maintenance (BGProcessingTask), foreground-initiated work that continues in background (BGContinuedProcessingTask), background downloads, and push notifications. Use this when your app needs to perform work while in the background or when the user isn't actively using it.
- Register and schedule BGAppRefreshTask for short-lived background fetches (~30 seconds)
- Configure BGProcessingTask for long-running maintenance work when device is idle and optionally charging
- Implement BGContinuedProcessingTask (iOS 26+) for foreground-started work that continues in background with Live Activity progress
- Handle background URLSession downloads and background push notification triggers
- Manage task expiration handlers and graceful cancellation when system revokes time
- Configure Info.plist with BGTaskSchedulerPermittedIdentifiers and UIBackgroundModes
How to install background-processing
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill background-processing- Xcode project with iOS 13+ target (iOS 26+ for BGContinuedProcessingTask)
- Enable Background Modes capability: Background fetch and Background processing
- Declare task identifiers in Info.plist under BGTaskSchedulerPermittedIdentifiers
How to use background-processing
- 1.Register task handlers in app delegate (UIKit) or App.init() (SwiftUI) before launch completes
- 2.Declare task identifiers in Info.plist BGTaskSchedulerPermittedIdentifiers array
- 3.Create and submit task requests (BGAppRefreshTaskRequest, BGProcessingTaskRequest, or BGContinuedProcessingTaskRequest) with appropriate configuration
- 4.Implement handler functions that schedule the next task, perform work asynchronously, and call setTaskCompleted()
- 5.Set expirationHandler on each task to cancel in-flight work and clean up when system revokes time
- 6.Test with simulated launches using Xcode scheme arguments or e -l com.apple.CoreLocation.simulatedLaunchEvent
Use cases
- Fetch fresh feed data or notifications periodically while app is backgrounded
- Clean up database records, rebuild indexes, or perform maintenance during idle device time
- Export photos or process large datasets initiated by user, continuing in background with progress tracking
- Download files in background without blocking foreground app interaction
- Respond to background push notifications with custom processing logic
- iOS app developers implementing background sync or data refresh
- Apps requiring periodic maintenance tasks (database cleanup, cache management)
- Content-heavy apps needing background downloads or exports
- Apps using push notifications with custom background handling
background-processing FAQ
Use BGAppRefreshTask for short operations (~30 seconds) like fetching feed updates. Use BGProcessingTask for longer maintenance work (minutes) that should run when device is idle and optionally charging, such as database cleanup or index rebuilding.
earliestBeginDate is only a lower-bound hint; the system decides actual launch based on device usage patterns, battery, and resource availability. The system may delay or skip tasks. Always set expirationHandler to gracefully handle early termination.
The system can revoke time at any moment. Without expirationHandler, your task may be forcibly terminated mid-operation, leaving partial data or corrupted state. Always cancel in-flight work and clean up before calling setTaskCompleted().
No, BGContinuedProcessingTask requires iOS 26+. For earlier versions, use BGAppRefreshTask or BGProcessingTask instead.
Register handlers before app launch, then use Xcode scheme arguments or simulated launch events to trigger tasks. Set breakpoints in handler functions. Check Console for BGTaskScheduler errors like notPermitted (missing Info.plist entry).
Full instructions (SKILL.md)
Source of truth, from dpearson2699/swift-ios-skills.
name: background-processing description: "Schedule 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, background URLSession downloads, or background push notifications. Covers Info.plist configuration, expiration handling, task completion, and debugging with simulated launches."
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 time
5. 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.
- BGTaskScheduler
- BGAppRefreshTask
- BGProcessingTask
- BGContinuedProcessingTask (iOS 26+)
- BGContinuedProcessingTaskRequest (iOS 26+)
- Using background tasks to update your app
- Performing long-running tasks on iOS and iPadOS
Related skills
More from dpearson2699/swift-ios-skills and the wider catalog.

browserenginekit
Build alternative browser engines for iOS/iPadOS with process isolation, XPC communication, and system integration.

callkit
Implement native iOS VoIP calling with CallKit and PushKit integration.

carplay
Build CarPlay-enabled apps for vehicle displays using Apple's template-based UI framework.

cloudkit
Implement CloudKit and iCloud sync for iOS/macOS apps with record CRUD, queries, subscriptions, and SwiftData integration.

contacts-framework
Read, create, update, and pick contacts using Contacts and ContactsUI frameworks.

core-bluetooth
Build Bluetooth Low Energy central and peripheral workflows with Core Bluetooth scanning, connecting, GATT communication, and background modes.