4.9 — Background execution
Opening scenario
User taps your podcast app’s “download episode” button, then locks the phone and shoves it in a pocket. Twenty minutes later, on the bus, they pull out the phone, open the app — and the episode is downloaded, the next-up queue refreshed, listening history synced.
iOS is aggressive about suspending apps. The OS prefers your app uses zero CPU, zero radio, zero battery when not foregrounded. Background execution is a system of explicit, narrow permissions: each one says “you may do this specific thing for this much time.”
| Need | API |
|---|---|
| Finish a task user just kicked off (~30s) | UIApplication.beginBackgroundTask |
| Periodic refresh (“update content overnight”) | BGAppRefreshTask |
| Heavy work on power + Wi-Fi (“re-index database”) | BGProcessingTask |
| Download / upload that survives app suspension | Background URLSession |
| Audio playing while screen off | Audio background mode |
| Location updates in background | Location background mode + entitlement |
| Server-pushed updates | Silent push notifications |
Concept → Why → How → Code
App lifecycle recap
In iOS, your process states (from UIScene.activationState / app delegate):
- Not running — never launched, or terminated by user/OS
- Inactive — foreground but not receiving events (e.g., user pulled down Control Center)
- Active — foreground and receiving events
- Background — runs briefly after going to background; will be suspended
- Suspended — frozen in memory; OS may kill it any time
Background tasks let you do work in state 4 before becoming suspended, or get launched into state 4 for periodic work.
beginBackgroundTask — finish what you started
When the user backgrounds the app mid-operation (uploading a comment, saving a draft), you get ~30 seconds to finish:
let taskID = UIApplication.shared.beginBackgroundTask(withName: "SubmitComment") {
// Expiration handler — called when time runs out
cleanUp()
UIApplication.shared.endBackgroundTask(taskID)
}
Task {
defer { UIApplication.shared.endBackgroundTask(taskID) }
do {
try await api.send(commentRequest)
} catch {
log(error)
}
}
Rules:
- Always call
endBackgroundTask— even on error, even in the expiration handler. Failing to do so eventually crashes the app for hogging background time. - Pair with
beginBackgroundTask1:1. You can have multiple concurrent task IDs. - Don’t expect more than ~30 seconds. Earlier iOS versions gave 3 minutes; modern iOS is stingier.
Use case: user submits a form, hits home before the request finishes. Without beginBackgroundTask, the app suspends instantly and the request fails.
BGTaskScheduler — periodic background work
For “every now and then, refresh content” or “occasionally re-process data,” use BackgroundTasks framework (iOS 13+).
Register identifiers in Info.plist:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.myapp.refresh</string>
<string>com.myapp.cleanup</string>
</array>
Register handlers at launch (must happen before application(_:didFinishLaunchingWithOptions:) returns):
import BackgroundTasks
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.myapp.refresh", using: nil) { task in
handleRefresh(task: task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.myapp.cleanup", using: nil) { task in
handleCleanup(task: task as! BGProcessingTask)
}
Schedule work when the app goes to background:
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.myapp.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60) // earliest 1 hour from now
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}
iOS decides when (based on usage patterns, power, network). You get called when it picks your moment.
Handler — finish quickly (~30s for refresh, longer for processing) and must call setTaskCompleted:
func handleRefresh(task: BGAppRefreshTask) {
scheduleAppRefresh() // chain the next one
task.expirationHandler = {
// OS reclaiming time; cancel ongoing work
currentTask?.cancel()
}
currentTask = Task {
do {
try await refreshContent()
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
}
BGProcessingTask is for heavier work that needs power and/or network — re-indexing local DB, downloading large updates. You can require requiresNetworkConnectivity = true and requiresExternalPower = true so the OS only triggers when plugged in on Wi-Fi.
Test the handler manually — iOS won’t run it on demand for you. From LLDB while paused:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.myapp.refresh"]
(This is a private API — debug only.)
Background URLSession
Covered in 4.8 but recapped: URLSessionConfiguration.background(withIdentifier:) keeps downloads/uploads running after suspension. OS launches your app in background on completion via application(_:handleEventsForBackgroundURLSession:completionHandler:). Implement the delegate to save the completionHandler, finalize, then invoke it so iOS knows you’re done.
Silent push notifications
For server-pushed background refresh (e.g., new message arrived, update local cache before user opens app):
- Enable “Remote Notifications” capability + “Background Modes → Remote notifications”
- Server sends APNs payload with
"content-available": 1:
{
"aps": { "content-available": 1 },
"messageId": "abc123"
}
- iOS wakes your app and calls:
func application(_ app: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
do {
try await syncNewMessages(triggeredBy: userInfo)
return .newData
} catch {
return .failed
}
}
Return one of .newData / .noData / .failed so iOS calibrates how often to wake you.
Caveats:
- iOS may throttle silent pushes (rate-limit, defer). Not guaranteed delivery for waking up the app.
- User can disable “Background App Refresh” in Settings — your silent pushes won’t wake the app.
- Don’t use silent push for time-critical actions; use a regular alerting push.
Audio in background
For podcast / music apps, enable Audio background mode:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
Configure the audio session for playback:
import AVFoundation
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [])
try AVAudioSession.sharedInstance().setActive(true)
Now AVPlayer.play() continues after lock screen. Combine with MPNowPlayingInfoCenter to show track info on lock screen and Control Center:
import MediaPlayer
MPNowPlayingInfoCenter.default().nowPlayingInfo = [
MPMediaItemPropertyTitle: episode.title,
MPMediaItemPropertyArtist: episode.showName,
MPMediaItemPropertyPlaybackDuration: episode.duration,
MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime().seconds
]
let cmd = MPRemoteCommandCenter.shared()
cmd.playCommand.addTarget { _ in player.play(); return .success }
cmd.pauseCommand.addTarget { _ in player.pause(); return .success }
Location in background
Two modes:
- Significant location changes (battery-friendly, ~500m precision) — works fully suspended
- Standard continuous updates (precise but battery-hungry) — requires
Alwaysauthorization + entitlement
let manager = CLLocationManager()
manager.delegate = self
manager.requestAlwaysAuthorization()
manager.allowsBackgroundLocationUpdates = true
manager.pausesLocationUpdatesAutomatically = true
manager.startMonitoringSignificantLocationChanges()
For Uber-style ride-tracking, enable showsBackgroundLocationIndicator = true (blue bar on top while tracking — required for Always-authorized continuous updates).
Combining modes
Many apps combine: audio + remote notifications + background fetch. Each requires its own UIBackgroundModes entry and matching capabilities. Apple reviews these; lying about why you need them is a fast App Store rejection.
Energy & responsibility
Background execution costs battery. iOS measures this and exposes it to the user (Settings → Battery → app usage). Apps with bad reputation get throttled — silent pushes ignored, BGAppRefresh rarely scheduled.
Best practices:
- Do the minimum work each background invocation
- Coalesce: if you need to sync 5 things, do them in one background pass, not five
- Respect
Low Power Mode(ProcessInfo.processInfo.isLowPowerModeEnabledistrue) — skip non-essential refreshes - Use Wi-Fi when available (
config.allowsCellularAccess = falseon opportunistic syncs)
Debugging
- Console.app on macOS with the device connected shows OSLog messages and system “ran your background task” entries
- Xcode → Debug → Simulate Background Fetch triggers the legacy fetch API (deprecated, but somewhat useful)
- Xcode → Debug → Simulate Push Notification with a JSON file triggers silent pushes in the simulator
- Settings → Developer → Background Task on iOS device gives you manual triggers
- OSLog with
subsystemandcategory— filter system logs to your app’s background activity
In the wild
- Spotify uses background audio + background URL sessions for downloaded podcasts/songs.
- Strava uses background location (significant changes + standard with paused updates between activities).
- Pocket prefetches articles using
BGAppRefreshTaskovernight on Wi-Fi. - Slack uses silent pushes to pre-fetch new messages; your app shows them instantly on next open.
- Apple Photos does heavy ML re-indexing (faces, objects) via
BGProcessingTaskwithrequiresExternalPower = true— runs at night while charging.
Common misconceptions
- “
beginBackgroundTaskgives me unlimited time.” No — ~30 seconds max. Expiration handler fires; you must clean up. - “
BGAppRefreshTaskruns on a schedule I set.” You request an earliest time. iOS decides actual scheduling based on usage patterns, power, network. Could be 1 hour from now, could be 12 hours. - “Silent push is reliable for delivery.” It’s best-effort. APNs may coalesce, defer, or drop them — especially if your app’s battery reputation is bad.
- “I can use background modes to run arbitrary code.” No — each background mode unlocks one specific capability. Apple reviews and rejects misuse.
- “Background fetch is the same as
BGAppRefresh.”UIApplication.backgroundFetchis deprecated. UseBGTaskSchedulergoing forward.
Seasoned engineer’s take
Background execution is where principled architecture pays off. Lessons:
- Cheap, idempotent work is best. Background invocations may be interrupted, repeated, or skipped. Your sync logic must handle that gracefully (cursor + idempotent merge).
- Always log what you do in background. OSLog with privacy-correct markers. When users report “the app didn’t refresh,” you need traces.
- Respect the system. Apple measures battery impact and throttles abusers. Earn your background time by being lean and useful.
- Defer to push when you can. Silent push is more reliable than
BGAppRefreshfor “something changed on the server.” SaveBGAppRefreshfor housekeeping. - Test on real devices, in real conditions. Simulator doesn’t reflect iOS’s actual scheduling. Use TestFlight + telemetry to confirm your background tasks actually run.
TIP: When chaining
BGAppRefreshTask, always schedule the next one first, before doing the work. If your work crashes, at least the system still knows you want to be called again.
WARNING: Modifying SwiftUI views or
UIKitUI from a background task handler will crash. UI updates must come from the main actor.await MainActor.run { ... }or@MainActorannotate the relevant code.
Interview corner
Junior-level: “What’s the difference between viewWillDisappear and applicationDidEnterBackground?”
viewWillDisappear is per-VC (the view is being removed from screen — could be a push, modal dismiss, tab switch). applicationDidEnterBackground (now sceneDidEnterBackground with scene API) is app-level (the user backgrounded the app). Different cleanup work belongs in each — release expensive per-screen resources in the former, save state and snapshot in the latter.
Mid-level: “How would you implement ‘sync user’s data periodically when the app isn’t open’?”
Register a BGAppRefreshTaskRequest with identifier com.app.sync, earliestBeginDate 1 hour out. Handler: schedule next, kick off async sync, on completion call task.setTaskCompleted(success:). Expiration handler cancels the sync Task. Combined with silent pushes for time-sensitive updates. Test by triggering manually via private LLDB call in debug builds and via TestFlight in production.
Senior-level: “Design a podcast app that downloads episodes overnight on Wi-Fi while charging, plays them with lock-screen controls, and handles network changes gracefully.”
Downloads: BGProcessingTaskRequest with requiresExternalPower = true and requiresNetworkConnectivity = true. Handler uses a background URLSession to download episode files. Persistence: track download state per episode in Core Data; URLSessionDelegate updates progress + final URL. App-foreground rebinds the background session to continue progress UI. Playback: AVAudioSession .playback category; MPNowPlayingInfoCenter for lock-screen UI; MPRemoteCommandCenter for play/pause/skip. Audio interruption (call, alarm) handled via AVAudioSession.interruptionNotification. Connectivity changes via NWPathMonitor; if user switches from Wi-Fi to cellular mid-download, pause if “Wi-Fi only downloads” preference set. Energy: skip pre-fetching in Low Power Mode. Telemetry: log background task start/end with episode IDs, processing time, failures — surface in dashboards.
Red flag in candidates: Trying to keep the app “alive” via abuse of background audio (silent audio loop) or location (no real location use case). Apple rejects these and users uninstall battery-drainers.
Lab preview
Background work doesn’t feature in Phase 4 labs directly (UIKit fundamentals focus); covered with real implementation in Phase 6 (SwiftUI + Combine) and Phase 11 (production app).
Next: 4.10 — UIKit + Combine