iOS constantly moves your app between foreground, background, and suspended, and creates and tears down views as the user navigates. Handle the app scene phase and the UIViewController lifecycle correctly.
iOS drives your whole app through phases — active (foreground, interactive), inactive (transitioning), and background (not visible, soon to be suspended). You react to these to pause work, save state, and refresh on return. In SwiftUI you observe the scenePhase environment value; getting this right is what makes an app feel correct when the user switches away and back.
import SwiftUI
struct MyApp: App {
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup { ContentView() }
.onChange(of: scenePhase) { _, phase in
switch phase {
case .active: print("foreground — resume, refresh")
case .inactive: print("transitioning")
case .background: print("save state, pause work")
@unknown default: break
}
}
}
}UIKit screens are UIViewControllers with a lifecycle the system calls as the view appears and disappears: viewDidLoad (once, set up), viewWillAppear/viewDidAppear (each time it becomes visible), viewWillDisappear/viewDidDisappear (as it leaves). You acquire resources when visible and release them when not — the same acquire/release discipline that keeps any mobile app efficient and leak-free.
viewDidLoad once, after the view loads — one-time setup
|
viewWillAppear each time it's ABOUT to become visible
viewDidAppear each time it IS visible — start work (timers, analytics)
| (user navigates away)
viewWillDisappear about to leave — save, pause
viewDidDisappear gone — release resources (stop timers, cancel requests)
Rules of thumb:
viewDidLoad set up UI + one-time init
appear/disappear acquire while visible, RELEASE promptly when not
Getting acquire/release paired with visibility is most of UIKit correctness.SwiftUI views are lightweight value types recreated freely, so their "lifecycle" is different: you run work when a view appears with onAppear/onDisappear, and — better for async — the task modifier, which starts an async job tied to the view’s lifetime and cancels it automatically when the view goes away. This is how you load data when a screen shows.
struct ProfileView: View {
let userId: Int
@State private var name = "Loading..."
var body: some View {
Text(name)
.task { // runs async when the view appears
name = await fetchName(userId) // cancelled automatically on disappear
}
.onAppear { print("appeared") }
.onDisappear { print("gone") }
}
}
// 'task' ties an async job to the view's lifetime -> no leaked work.
// SwiftUI views are cheap value types; don't store heavy state in them.When iOS suspends your app it may later terminate it to reclaim memory, so you save enough state to restore the user where they left off, and you finish or defer in-flight work when heading to the background. iOS grants only brief background time; long tasks use background tasks or push-driven refresh. Respecting these limits is what keeps an app within Apple’s energy rules.
When your app goes to the BACKGROUND, iOS may later kill it. So:
- SAVE state on entering background (scenePhase .background) -> restore on launch
- FINISH or checkpoint in-flight work; you get only seconds of background time
- need more time? beginBackgroundTask(...) for a short extension
- periodic/deferred work? BGTaskScheduler (background app refresh)
- server-driven updates? push notifications wake the app
Assume the app can be terminated at any time in the background. Design so a
cold launch restores the user's place seamlessly.