Active, inactive, background
Think of a shop assistant. While serving a customer, they’re active. When the phone rings mid-conversation, they’re inactive: still there, but not serving. When they go to the back room, they’re in the background; and if the shop closes for the day, the lights go off without anyone asking whether they’ve finished their notes.
An iOS app’s screen (its scene) moves through the same phases. The important part: after a few seconds in the background, iOS suspends the app (freezes it), and later it may terminate it to free memory, with no further warning.
Scene phases (ScenePhase): .active (foreground, receiving events), .inactive (foreground but not receiving events: app switcher, Control Center, incoming call overlay, transitions), .background (not visible; a few seconds of execution, then suspended unless the app holds a background assertion or mode). Suspended apps receive no callbacks before being jetsammed; applicationWillTerminate is only called if the app is terminated while running in the background, not when suspended or swiped away from the switcher. The contract: persist at .background, release shared resources at .background, and restore everything from storage on launch.
struct ComposeView: View {
@Environment(\.scenePhase) private var scenePhase
@State private var draft = DraftStore.load()
var body: some View {
TextEditor(text: $draft)
.onChange(of: scenePhase) { _, phase in
switch phase {
case .background:
DraftStore.save(draft) // last reliable moment to save
case .inactive:
break // a call, Control Center, the app switcher
case .active:
break // back in front: refresh if needed
@unknown default:
break
}
}
}
}| Phase | When | What to do |
|---|---|---|
.active | In front, the user can tap | Normal work; refresh stale data on return |
.inactive | Briefly interrupted: a call banner, Control Center, the app switcher | Pause games and timers; nothing heavy |
.background | The user went home or switched apps | Save user data, stop location and timers you don’t need |
After .background, there’s no promise the app will ever run again before it’s terminated. Whatever must survive has to be saved by then.
Tap the line with the bug.
struct ReviewEditor: View {
@State private var text = DraftStore.load()
var body: some View {
TextEditor(text: $text)
.navigationTitle("Your review")
.onDisappear {
DraftStore.save(text)
}
}
}Hint: The user switches to Messages for an hour. Does ReviewEditor disappear from the view hierarchy? And what happens to the app while it's in the background?
onDisappear runs when the view leaves the view hierarchy (the user navigates back), not when the app goes to the background: switching apps leaves the view where it is. Meanwhile the app is suspended, and if iOS needs memory, terminated, without running any more of your code. The draft was never saved.
Save when the scene goes to the background, and periodically while the user types:
struct ReviewEditor: View {
@Environment(\.scenePhase) private var scenePhase
@State private var text = DraftStore.load()
var body: some View {
TextEditor(text: $text)
.onChange(of: scenePhase) { _, phase in
if phase == .background { DraftStore.save(text) }
}
.task(id: text) { // a debounced autosave while typing
try? await Task.sleep(for: .seconds(2))
if !Task.isCancelled { DraftStore.save(text) }
}
.onDisappear { DraftStore.save(text) } // still useful when navigating back
}
}Predict the output
The app is active. The user swipes down to open Control Center, closes it, then swipes up to go to the Home Screen. Which phase changes does the app see?
Control Center covers the app without leaving it: inactive, and active again when it's closed. Going home passes through inactive first (during the animation), then background. That's why heavy work shouldn't run on every .inactive: it happens for small interruptions all the time. Save on .background.
Autosave a form
A “New listing” form has a title, a price and a description. Make sure nothing the user typed is lost if they leave the app (even if iOS terminates it later), and that the form comes back filled in on the next launch.
›
You need a place on disk (UserDefaults is fine for a small draft, or a file, or SwiftData), a save when the scene goes to the background, and a load when the form is created.
SolutionTry it yourself first
struct ListingDraft: Codable, Equatable {
var title = ""
var price = ""
var description = ""
static let key = "listing-draft"
static func load() -> ListingDraft {
guard let data = UserDefaults.standard.data(forKey: key),
let draft = try? JSONDecoder().decode(ListingDraft.self, from: data) else { return ListingDraft() }
return draft
}
func save() {
if let data = try? JSONEncoder().encode(self) { UserDefaults.standard.set(data, forKey: Self.key) }
}
}
struct NewListingForm: View {
@Environment(\.scenePhase) private var scenePhase
@State private var draft = ListingDraft.load()
var body: some View {
Form {
TextField("Title", text: $draft.title)
TextField("Price", text: $draft.price).keyboardType(.decimalPad)
TextField("Description", text: $draft.description, axis: .vertical)
}
.onChange(of: scenePhase) { _, phase in
if phase == .background { draft.save() }
}
.onDisappear { draft.save() }
}
}To test termination: run the app from Xcode, type something, go to the Home Screen, then press Stop in Xcode (which terminates the suspended app without any callback). Launch again: the draft should be there. Clear it after the listing is published.
Launch, restoration and memory
- Cold launches restore nothing by themselves. SwiftUI’s
@SceneStoragerestores small per-scene UI state (selected tab, navigation path encoded asData) after termination; app data comes from your store. - Memory pressure: the app may receive memory warnings in the foreground; in the background, it’s simply terminated. Large caches should shrink on
.background(andNSCachedoes it automatically), so the app is less likely to be the one jetsammed. - Multiple scenes (iPad, Mac): each scene has its own phase; app-wide work (sync) should follow the app’s overall state, not one window’s.
- Protected data: with Data Protection, files become unreadable shortly after the device locks (
NSFileProtectionComplete); background work that reads them fails. Choose the protection class per file deliberately.
ScenePhase:.active,.inactive(frequent, brief: app switcher, Control Center, calls),.background(seconds, then suspension). Suspended apps are terminated without callbacks;applicationWillTerminateisn’t reliable.- Persist at
.backgroundplus debounced autosave;onDisappeardoesn’t fire on backgrounding. @SceneStoragefor small UI restoration; shrink caches on background; per-scene phases on iPad/Mac; file protection classes affect background reads.
Check this part
When is the last reliable moment to save what the user typed?
Does onDisappear run when the user switches to another app?
Is applicationWillTerminate called when a suspended app is terminated by the system?