Background work is where many SwiftUI apps quietly fail.
Symptoms:
- sync stops when app closes
- notifications arrive late
- background refresh never fires
- battery drains
- tasks get killed
- “it works on simulator” lies
SwiftUI doesn’t change how background execution works — it just makes it easier to ignore.
This post shows how to design reliable, battery-safe background work architecture in SwiftUI:
- background refresh
- processing tasks
- silent pushes
- task coordination
- state recovery
- real-world constraints
No myths. No magic. Just how it actually works.
🧠 The Core Principle
Background execution is a contract, not a guarantee.
The system decides:
- when
- if
- how long
- with what priority
Your job is to prepare work, not demand it.
🧱 1. The Three Background Mechanisms
1. Background App Refresh
- periodic
- opportunistic
- short execution window
2. Background Processing Tasks
- longer running
- requires power + network
- scheduled by the system
3. Silent Push Notifications
- server-triggered
- immediate (sometimes)
- unreliable delivery
You usually need all three.
⚙️ 2. Register Background Tasks
In AppDelegate or app entry:
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.app.refresh",
using: nil
) { task in
handleRefresh(task: task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.app.processing",
using: nil
) { task in
handleProcessing(task: task as! BGProcessingTask)
}
Identifiers must be in:
- Info.plist
- App Capabilities
🔄 3. Scheduling Tasks
func scheduleRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(request)
}
func scheduleProcessing() {
let request = BGProcessingTaskRequest(identifier: "com.app.processing")
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false
try? BGTaskScheduler.shared.submit(request)
}
You request. The system decides.
🧩 4. Handling the Task
func handleRefresh(task: BGAppRefreshTask) {
scheduleRefresh() // Always reschedule
let operation = Task {
await syncData()
}
task.expirationHandler = {
operation.cancel()
}
Task {
await operation.value
task.setTaskCompleted(success: true)
}
}
Key rules:
- always reschedule
- always handle expiration
- always call
setTaskCompleted
🧠 5. Background Work Architecture
Never do work directly in the handler.
Instead:
func syncData() async {
await repository.sync()
}
Your background task calls the same code path as:
- manual refresh
- pull-to-refresh
- app launch sync
One pipeline. Multiple triggers.
🌐 6. Silent Push Triggers
Payload:
{
"aps": {
"content-available": 1
},
"type": "sync"
}
Handler:
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
Task {
await syncData()
completionHandler(.newData)
}
}
Never rely on silent pushes alone.
They are best-effort.
🔋 7. Battery & Priority Design
Not all work is equal.
Define priority:
enum SyncPriority {
case critical
case normal
case low
}
Then:
func sync(priority: SyncPriority) async {
switch priority {
case .critical:
await performCriticalSync()
case .normal:
await performNormalSync()
case .low:
break // defer
}
}
Background tasks should never do everything.
🧬 8. Partial Sync & Checkpointing
Never assume you’ll finish.
Design for interruption:
func syncStep1() async throws { }
func syncStep2() async throws { }
func syncStep3() async throws { }
Checkpoint:
saveProgress(step: 2)
On next run, resume.
This is how large syncs survive background limits.
🧪 9. Testing Background Behavior
Simulate in Xcode:
- Debug > Simulate Background Fetch
- Debug > Simulate Silent Push
Also test:
- low battery
- low signal
- airplane mode
- app force quit
Background code that only works on Wi-Fi + power is broken.
❌ 10. Common Anti-Patterns
Avoid:
- doing heavy work in view .task
- assuming background always runs
- blocking the main thread
- not handling expiration
- not rescheduling tasks
- syncing everything every time
- ignoring battery impact
These cause:
- kills
- throttling
- App Store rejections
🧠 Mental Model
Think:
Trigger (system / push / user)
→ Task Request
→ Limited Execution Window
→ Small, Critical Work
→ Checkpoint
Not:
“Run my sync in background”
🚀 Final Thoughts
A real background architecture gives you:
- reliable sync
- timely updates
- better battery life
- fewer data bugs
- happier users
Background work is not a feature.
It’s infrastructure.
Design it like one.
Top comments (0)