Platform integrations · core

App lifecycle and background execution

Scene phases, the seconds you get when the user leaves, and the honest map of what iOS actually permits in the background.

13 min read12 min practice0/2 exercises4 recall cards

By the end you will be able to

  • React to scene-phase transitions with the right work at each one
  • Answer "user backgrounds mid-upload" with the grace-period vs background-session split
  • Schedule BGTaskScheduler work with honest expectations about when it runs
Guess firstAnswering before you read makes the explanation stick — even when you get it wrong.

The evergreen interview question: "the user starts a 200MB upload and immediately swipes to the home screen. What happens to the upload?" Which answer is senior?

The lifecycle in SwiftUI: three phases

The modern API is one environment value and a modifier:

@main
struct CheckoutApp: App {
    @Environment(\.scenePhase) private var scenePhase
    @State private var model = AppModel()

    var body: some Scene {
        WindowGroup { RootView(model: model) }
            .onChange(of: scenePhase) { _, phase in
                switch phase {
                case .active:      model.refreshIfStale()      // came to foreground
                case .inactive:    model.pauseSensitiveWork()  // visible, not interactive
                case .background:  model.saveEverything()      // may be suspended/killed next
                @unknown default:  break
                }
            }
    }
}

The three phases, with the job of each:

  • .active — foreground, receiving events. On entry: refresh stale data, resume timers/animations, re-check permissions the user may have changed in Settings.
  • .inactive — visible but not interactive: mid-transition, system dialog up, app-switcher swipe in progress. On entry: pause gameplay/video; this is also where privacy-sensitive apps put up their cover so the app-switcher snapshot hides content.
  • .background — off screen. This is a deadline: you have seconds before suspension, and no promise of ever running again — the next event may be termination without notice. Save all user state now.

That last sentence is a rule with a name worth using: background = save point. Apps that lose a half-composed message when the user answers a phone call skipped it; nothing else on this page matters if this part is wrong. (And note what phases don't tell you: .background fires equally for "checking Messages for five seconds" and "gone for a week" — persist as if it's forever, every time.)

The suspension timeline, and the grace period

What actually happens after the swipe:

foreground ──▶ .inactive ──▶ .background ──▶ [~seconds] ──▶ SUSPENDED ──▶ maybe TERMINATED
                                              │                             (memory pressure,
                                              └── beginBackgroundTask ──┐    no notice)
                                                  extends to ~30s        │
                                                  expiration handler ────┘

beginBackgroundTask(expirationHandler:) requests a short extension — historically minutes, now on the order of ~30 seconds — for finishing an operation already in flight: flushing the outbox, completing a save, closing a file cleanly. The contract has two teeth: you must call endBackgroundTask when done (leaking the token gets the app killed for overrunning), and the expiration handler must end the work immediately when the system calls time. It is a grace period, not a work session.

For anything bigger than the grace period, you hand the work to the system:

NeedMechanismHonest expectation
finish in-flight save/flushbeginBackgroundTaskseconds; guaranteed only that you get warning before cutoff
large upload/downloadbackground URLSessioncompletes even if app terminates; app woken on finish
periodic refreshBGAppRefreshTaskdiscretionary — runs when the system chooses, if the user uses your app
long maintenance (index, prune, ML)BGProcessingTaskovernight-ish: idle, often charging; can request power/network
content pushsilent notification (content-available)budgeted, unreliable — never the backbone (notifications lesson)
continuous audio/location/callsbackground modes entitlementsgenuinely continuous, but reviewed and battery-visible

The two rows interviews probe:

Background URLSessionURLSessionConfiguration.background(withIdentifier:). Transfers run in a system daemon, surviving your suspension and termination; when they finish, iOS relaunches the app in the background and calls the session delegate. The integration quirk everyone hits: results arrive via the delegate (not async closures), and the app must reconnect to the session by the same identifier on relaunch.

BGAppRefreshTask — the "check for new content periodically" API, and the one whose expectations are most often wrong. You submit a request with an earliestBeginDate; the system decides if and when it runs, weighing user habits, battery, and your app's track record. It is not a timer, not cron, and heavy users get more runs than abandoned installs. Design so refresh is a bonus, never a dependency — the offline-sync lesson's outbox already made foreground drains correct; background refresh just makes them fresher.

The suspension timeline as a state machine — the save-point rule and the grace-period contract.

Both traces end with unsaved edits at risk: 0 — because the save happened at the phase transition, before any grace-period gamble. The flush racing the expiration handler is the outbox drain, which was already safe to interrupt (idempotency keys, persistent queue) — the sync lesson's design decisions are what make this lesson's deadlines survivable.

Check yourself

Your podcast app must download new episodes "overnight". A teammate schedules a BGAppRefreshTask and reports it "only ran twice this week" on their test device. What is the correct reading?

Route the work

Write the code

Encode the mechanism table: given a job's shape, return the right background mechanism — the interview answer as a function.

Solution unlocks after 3 attempts

Handle every phase

Write the code

Implement the phase handler with the save-point rule and the app-switcher privacy cover — then trace a phone call arriving mid-edit.

Solution unlocks after 3 attempts
RecallSaved on this device. Never graded.

Answer the interview question from memory, out loud: "the user starts a big upload, backgrounds the app, and the system terminates it two minutes later — walk me through what happens at each step and what code makes the upload survive." Include the phase transitions, why a grace period is insufficient, the daemon handoff, and the relaunch path. Then check against the lesson.

Checkpoint

You can now:

  • Handle all three scene phases with the save-point rule and the privacy cover
  • Choose grace period vs background URLSession vs BG tasks per job shape
  • Set honest expectations for discretionary scheduling
  • Answer the backgrounded-upload question end to end

Next up: debugging, crash triage and CI — the craft of finding out what actually happened.

How well do you know this now? Rating yourself honestly, then being tested on it, is how you find out where your intuition is wrong.