---
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](#infoplist-configuration)
- [BGTaskScheduler Registration](#bgtaskscheduler-registration)
- [BGAppRefreshTask Patterns](#bgapprefreshtask-patterns)
- [BGProcessingTask Patterns](#bgprocessingtask-patterns)
- [BGContinuedProcessingTask (iOS 26+)](#bgcontinuedprocessingtask-ios-26)
- [Background URLSession Downloads](#background-urlsession-downloads)
- [Background Push Triggers](#background-push-triggers)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)
## Info.plist Configuration
Every task identifier **must** be declared in `Info.plist` under
`BGTaskSchedulerPermittedIdentifiers`, or `submit(_:)` throws
`BGTaskScheduler.Error.Code.notPermitted`.
```xml
BGTaskSchedulerPermittedIdentifiers
com.example.app.refresh
com.example.app.db-cleanup
com.example.app.export
```
Also enable the required `UIBackgroundModes`:
```xml
UIBackgroundModes
fetch
processing
```
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 the
`App` initializer.
### UIKit Registration
```swift
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 = main 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
```swift
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.
```swift
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(
identifier: "com.example.app.refresh"
)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
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.
```swift
func scheduleProcessingTask() {
let request = BGProcessingTaskRequest(
identifier: "com.example.app.db-cleanup"
)
request.requiresNetworkConnectivity = false
request.requiresExternalPower = true
request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60)
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.
```swift
import BackgroundTasks
func startExport() {
let request = BGContinuedProcessingTaskRequest(
identifier: "com.example.app.export",
title: "Exporting Photos",
subtitle: "Processing 247 items"
)
// .queue: begin as soon as possible if can't run immediately
// .fail: fail submission if can't run immediately
request.strategy = .queue
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.export",
using: nil
) { task in
let continuedTask = task as! BGContinuedProcessingTask
Task {
await self.performExport(task: continuedTask)
}
}
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)
}
```
Check whether the system supports the resources your task needs:
```swift
let supported = BGTaskScheduler.shared.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.
```swift
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:
```swift
// 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.
```json
{ "aps": { "content-available": 1 }, "custom-data": "new-messages" }
```
Handle in AppDelegate:
```swift
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:
```swift
UIApplication.shared.registerForRemoteNotifications()
```
## Common Mistakes
### 1. Missing Info.plist identifiers
```swift
// 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
// com.example.app.refresh
```
### 2. Not calling setTaskCompleted(success:)
```swift
// 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
```swift
// 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
```swift
// 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
```swift
// 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 `UIBackgroundModes` enabled (`fetch`, `processing`)
- [ ] Tasks registered before app launch completes
- [ ] `setTaskCompleted(success:)` called on every code path
- [ ] `expirationHandler` set and cancels in-flight work
- [ ] Next task scheduled inside the handler (re-schedule pattern)
- [ ] `earliestBeginDate` uses reasonable intervals (15+ min for refresh)
- [ ] Background URLSession uses delegate (not async/closures)
- [ ] Background URLSession file moved in `didFinishDownloadingTo` before return
- [ ] `handleEventsForBackgroundURLSession` stores and calls completion handler
- [ ] Background push payload includes `content-available: 1`
- [ ] `fetchCompletionHandler` called 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](https://sosumi.ai/documentation/backgroundtasks/bgtaskscheduler)
- [BGAppRefreshTask](https://sosumi.ai/documentation/backgroundtasks/bgapprefreshtask)
- [BGProcessingTask](https://sosumi.ai/documentation/backgroundtasks/bgprocessingtask)
- [BGContinuedProcessingTask](https://sosumi.ai/documentation/backgroundtasks/bgcontinuedprocessingtask) (iOS 26+)
- [BGContinuedProcessingTaskRequest](https://sosumi.ai/documentation/backgroundtasks/bgcontinuedprocessingtaskrequest) (iOS 26+)
- [Using background tasks to update your app](https://sosumi.ai/documentation/uikit/using-background-tasks-to-update-your-app)
- [Performing long-running tasks on iOS and iPadOS](https://sosumi.ai/documentation/backgroundtasks/performing-long-running-tasks-on-ios-and-ipados)