Skip to content
1 of 3 project slots open
Writing
Fundamentals · part 1 of 55 min read

The iOS app lifecycle, drawn

Five states, the scene callbacks between them, and the one transition iOS never tells you about.

Meer Habib

Senior Mobile Engineer · Chittagong

Most iOS bugs that "only happen sometimes" are lifecycle bugs. The user took a call, pulled down Control Center, switched apps for a minute, and came back to a screen that forgot what it was doing. If you know the five states an app moves through, and which ones you actually get told about, most of those bugs stop being mysteries.

Five states

Not runningno processInactivevisible, no eventsActiveforeground, eventsBackgroundrunning, brieflySuspendedin memory, frozenlaunchdidBecomeActivewillResignActivedidEnterBackgroundwillEnterForegroundseconds laterwoken: push, BGTaskmemory pressure: killed, no callback
Fig. 1App states and the scene callbacks that move between them.

Not running. There is no process. Either the app was never launched, or the system removed it.

Inactive. The app is on screen but not receiving events. You pass through here on launch, and you land here when something covers you for a moment: an incoming call, Control Center, the app switcher, a system alert.

Active. On screen and receiving touches. This is where your app spends its useful life.

Background. Off screen, still running code. You only stay here briefly, unless you have a reason the system accepts, like playing audio or a navigation session.

Suspended. Still in memory, but frozen. No code runs. Coming back from here is instant, which is why apps feel like they "remember" where you were.

The one arrow with no callback

Look at the dashed line in the diagram. When the system needs memory, it can remove a suspended app without telling it. There is no "you're about to be killed" callback for a suspended app. applicationWillTerminate only fires if your app is actually running when it's terminated, which is rare.

So the rule is simple:

Save what matters when you go to the background, not when you're about to die.

If the user is halfway through a form, write the draft somewhere durable in sceneDidEnterBackground. If you wait for termination, you'll wait forever.

Scenes, not just the app

Since iOS 13 the lifecycle is split in two. UIApplicationDelegate handles process-level events: launch, push registration, memory warnings. UISceneDelegate handles UI state, because an iPad can show several windows of the same app, each with its own state.

The scene callbacks are the ones you'll use most:

func sceneWillEnterForeground(_ scene: UIScene)   // Background → Inactive
func sceneDidBecomeActive(_ scene: UIScene)       // Inactive → Active
func sceneWillResignActive(_ scene: UIScene)      // Active → Inactive
func sceneDidEnterBackground(_ scene: UIScene)    // Inactive → Background

In SwiftUI the same idea is one value:

@Environment(\.scenePhase) private var phase
 
.onChange(of: phase) { _, new in
  if new == .background { saveDraft() }
}

.active, .inactive, .background. Suspended never shows up, because by definition your code isn't running to see it.

Getting time in the background

When you enter the background you get a short, unspecified amount of time. Treat it as seconds. If you're in the middle of something that must finish, like an upload, ask for a little more:

var task: UIBackgroundTaskIdentifier = .invalid
task = UIApplication.shared.beginBackgroundTask {
  // Time is up. Clean up and end, or the system ends you.
  UIApplication.shared.endBackgroundTask(task)
}
upload { UIApplication.shared.endBackgroundTask(task) }

For work that should happen later, schedule it with BGTaskScheduler. An app refresh task is short and runs when the system decides it's a good moment. A processing task can run longer, often while the phone is idle or charging. Neither is a timer. You're asking, not scheduling.

Being woken up

A suspended or not-running app can be brought back into the background for specific reasons:

  • Remote notifications with content-available. Useful, but budgeted and never guaranteed.
  • Background tasks you scheduled with BGTaskScheduler.
  • VoIP pushes through PushKit. When I built calling for Ringo, this was the strict one: every VoIP push must be reported to CallKit as an incoming call right away, or iOS stops delivering them.
  • Location, audio, Bluetooth if you declared the background mode and have a real reason.

Live Activities are a nice exception to all of this. Their content can be updated with push notifications while your app isn't running at all.

In React Native

AppState maps onto the same states:

AppState.addEventListener("change", (state) => {
  // "active" | "inactive" | "background"
  if (state === "background") persistDraft();
});

inactive is iOS only. It's the brief state you pass through during an incoming call or while the app switcher is open. Don't pause audio or tear down a socket on inactive; wait for background.

A checklist

  • Save drafts and unsent work on background, not on terminate.
  • Refresh stale data on active, not on launch. Launch is rare; coming back is constant.
  • Don't start heavy work on inactive. The user may just be peeking at Control Center.
  • Test the dashed line: background the app, open a few heavy apps, come back.

Next: the same story on Android, where the dashed line is even more common.

Building something like this?

Booking new projects for Q4. Replies within 24h.