There's a question I ask every iOS candidate I interview: "Walk me through what happens from the moment a user taps your app icon to the moment they're looking at your home screen."

Most developers answer this with a list of method names. Senior developers answer it with a mental model. Staff engineers answer it by asking a clarifying question first — "Are we talking UIKit or SwiftUI? What iOS deployment target?" — because the answer genuinely differs, and knowing when it differs is the whole point.

This article is the answer I wish existed when I was first wrapping my head around this. It's not a documentation mirror. It's the mental model, the gotchas, the real-world decisions, and the interview traps — all in one place.


The Lifecycle Before iOS 13: AppDelegate Owned Everything

Before iOS 13, the story was simple. One delegate, one window, one lifecycle. AppDelegate was the nerve centre of your app — it handled launch, foreground, background, termination, and owned the UIWindow. Everything lived in one place.

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // setup everything here
        return true
    }
}

Simple. Maybe a little monolithic. But predictable.

Then iPadOS arrived with multitasking, and Apple faced an architectural problem: the entire lifecycle was wired to a single UIWindow. You couldn't have multiple independent UI instances of the same app because the architecture physically didn't support it. AppDelegate.window was a single property — not an array, not a dictionary. One window. Period.

So in iOS 13, Apple split the responsibility.


The iOS 13 Split: AppDelegate + SceneDelegate

The key insight behind the iOS 13 redesign is this: there are two kinds of lifecycle events — process-level and UI-level. Before iOS 13, AppDelegate handled both. After iOS 13, they're separated:

  • AppDelegate → process-level events (launch, termination, memory warnings, push token registration)
  • SceneDelegate → UI-level events (window creation, foreground, background, active state)

Think of it like a restaurant. AppDelegate is the building — the lease, the utilities, the license. SceneDelegate is each dining room — the tables, the ambience, the customer experience. A building can have multiple dining rooms. That's the point.

What AppDelegate kept

MethodWhen calledReal-world use
application(_:didFinishLaunchingWithOptions:)Process starts, before any UIFirebase, analytics, push registration, Core Data setup
applicationWillTerminate(_:)App terminated from background (unreliable — not called if suspended)Last-resort cleanup only. Never your primary save point
application(_:configurationForConnecting:options:)New scene session being createdReturn scene configuration — usually handled by Info.plist
application(_:didDiscardSceneSessions:)User swipes away scene in app switcherClean up data tied to that scene session
applicationDidReceiveMemoryWarning(_:)System is low on memoryFlush image caches, release non-critical in-memory data

What moved to SceneDelegate

MethodTransitionReal-world use
scene(_:willConnectTo:options:)Not Running → InactiveOne-time window and root view controller setup
sceneWillEnterForeground(_:)Background → InactiveRefresh auth tokens, remove blur overlay, restart location
sceneDidBecomeActive(_:)Inactive → ActiveRestart timers, resume animations, begin data polling
sceneWillResignActive(_:)Active → InactiveBlur sensitive content, pause video/game, save text input
sceneDidEnterBackground(_:)Inactive → BackgroundYour primary save point. Persist everything here
sceneDidDisconnect(_:)Background → Not RunningRelease scene-specific resources. Do NOT delete permanent data

The direct replacements are clean:

Old AppDelegate methodNew SceneDelegate equivalent
applicationWillEnterForeground(_:)sceneWillEnterForeground(_:)
applicationDidBecomeActive(_:)sceneDidBecomeActive(_:)
applicationWillResignActive(_:)sceneWillResignActive(_:)
applicationDidEnterBackground(_:)sceneDidEnterBackground(_:)

One critical point: if your app has a Scene Manifest in Info.plist, the old AppDelegate UI methods are never called. iOS routes everything to SceneDelegate. If you migrated to SceneDelegate and left applicationDidBecomeActive in your AppDelegate thinking it still runs — it doesn't.


The 5 App States: The Mental Model That Matters

Every iOS app exists in one of five states at any given moment. Understanding what actually happens in each, beyond the definition, is what separates good engineers from great ones.

Not Running

The process doesn't exist. Either the app has never been launched, or it was terminated — by the system reclaiming memory, by the user force-quitting, or by a crash.

Inactive

The app is on screen but not receiving touch events. This is always a transitional state — a bridge between states, never a destination. It happens during:

  • The brief moment between launch and becoming fully interactive
  • A phone call or CallKit call overlay appearing
  • The user swiping up to the app switcher
  • Control Centre or Notification Centre being pulled down
  • Siri activating

The inactive state is fast — often milliseconds. But it matters enormously for apps with sensitive content or real-time media. A banking app should blur its content the moment it goes inactive, not when it goes to background. By background, iOS has already taken the app switcher screenshot.

What does not trigger inactive: system permission alerts (location, camera, microphone). Those present within your app's own scene context. Your app stays active.

Active

The app is in the foreground and receiving user events. This is where your app spends most of its life.

Background

The app is not visible but its code is still running — briefly. You have approximately five seconds. This is your primary save point. Design your app as if every background transition could be the last one — because sometimes it is.

What reaches background: the user pressing home, switching apps, the screen locking, or the system waking your app for a background task (in which case the app goes directly from Not Running to Background, never touching Active).

Suspended

This is the state most developers know least about, yet it explains the most user-visible behaviour. Suspended means the process is in memory but executing zero code. The CPU is doing nothing for your app. No callbacks, no timers, no network calls.

You cannot observe suspension. There is no delegate method. Your code simply stops running — you have no idea it happened.

The reason suspended exists is a brilliant piece of systems thinking: it gives users the illusion of apps always running, at near-zero battery cost. Resuming from suspension feels instant because the memory snapshot is intact. Cold launching from Not Running takes 1-3 seconds. The difference in perceived quality is enormous.

iOS moves apps from Background to Suspended when it decides they've had enough background time. It moves them from Suspended to Not Running only when it needs the memory back.

The practical implication: always save state when entering background. You cannot save it later.


The Call Order You Need to Know Cold

First cold launch

didFinishLaunchingWithOptions
  → scene(_:willConnectTo:options:)
    → sceneWillEnterForeground
      → sceneDidBecomeActive

User backgrounds the app

sceneWillResignActive
  → sceneDidEnterBackground
    → [Suspended — no callback]

User returns to the app

sceneWillEnterForeground
  → sceneDidBecomeActive

Note that scene(_:willConnectTo:) does not fire again on resume. It fires once, at cold launch. Every subsequent foreground cycle skips it and goes straight to sceneWillEnterForeground. This is a common source of bugs — developers put setup logic in willConnectTo that should actually be in sceneWillEnterForeground.

Phone call interruption (returns to active without backgrounding)

sceneWillResignActive
  → [call ends]
    → sceneDidBecomeActive

Notice: sceneDidEnterBackground is never called. sceneWillResignActive does not always lead to background. Anything you do in sceneWillResignActive must be safely reversible in sceneDidBecomeActive.


applicationWillTerminate — The Most Misunderstood Method

Every iOS developer has written cleanup code in applicationWillTerminate. Most of that code never runs.

Here's the truth: this method is only called when the app is terminated while actively running in the background (not suspended). In practice, almost every termination happens from the suspended state — the user swipes the app away, iOS needs memory, the device reboots. In all those cases, your app is already suspended. No callback. No warning. The process is just gone.

You have approximately five seconds when it does fire. If you take longer, iOS kills the process anyway.

The right pattern: save everything in sceneDidEnterBackground. Treat applicationWillTerminate as a bonus, not a guarantee. If your data integrity depends on this method being called, you have a bug.


The SwiftUI Lifecycle: A Clean Slate

SwiftUI, introduced in iOS 14, replaced both AppDelegate and SceneDelegate with a single declarative entry point.

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

That's it. No window property. No delegate methods. The entire lifecycle is managed declaratively.

Lifecycle events via scenePhase

@main
struct MyApp: App {
    @Environment(\.scenePhase) private var scenePhase

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { phase in
            switch phase {
            case .active:     // sceneDidBecomeActive equivalent
            case .inactive:   // sceneWillResignActive equivalent
            case .background: // sceneDidEnterBackground equivalent
            @unknown default: break
            }
        }
    }
}

scenePhase can be read at any level of the view hierarchy — App struct, Scene, or deep inside any View — and it flows down via SwiftUI's environment system automatically. No prop drilling. No notification centre subscriptions.

The important nuance of where you read scenePhase

  • Read at the App struct level → aggregate phase of all scenes
  • Read at the Scene level → that specific scene's phase
  • Read inside a View → the phase of the scene containing that view

On iPhone (always single scene), these are identical. On iPad with multiple windows, they can differ — one scene can be .active while another is .background.

The onChange gotcha on first launch

A common confusion: why don't I see .inactive logged on first launch?

onChange only fires when a value changes. On first launch, scenePhase starts at .inactive as its default value — there's no change to report. The first change onChange catches is the transition from .inactive to .active. So you only see .active printed.

If you use initial: true in iOS 17's updated onChange:

.onChange(of: scenePhase, initial: true) { old, new in
    print("\(old) → \(new)")
    // First launch: inactive → inactive, then inactive → active
}

The inactive → inactive is the initial fire confirming the default value. The inactive → active is the real transition.

When you still need AppDelegate in a SwiftUI app

SwiftUI's scenePhase doesn't give you everything. Some things still require UIApplicationDelegate:

  • Push notification device token (didRegisterForRemoteNotificationsWithDeviceToken)
  • Firebase setup (FirebaseApp.configure() needs didFinishLaunchingWithOptions)
  • Third-party SDK callbacks
  • Deep link handling via application(_:open:options:)

Use @UIApplicationDelegateAdaptor for these:

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        FirebaseApp.configure()
        setupPushNotifications()
        return true
    }
}

@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

SwiftUI manages the delegate's lifetime. You don't instantiate it manually.

Keep AppDelegate lean. Delegate each concern to a dedicated private method or service object. didFinishLaunchingWithOptions that runs 200 lines of setup code is technical debt masquerading as convenience.


Multiple Scenes: What It Actually Means

iPadOS supports three forms of multi-app/multi-window interaction that are often conflated:

Split View — two different apps share the screen. Each runs in its own scene. Both can be .active simultaneously. This works automatically — you don't need to do anything.

Multiple Windows — the same app runs twice, side by side. Two scenes, same bundle. Each SceneDelegate instance is completely independent. This requires UIApplicationSupportsMultipleScenes = true in Info.plist and an architecture that doesn't assume a single shared mutable state. Singletons that hold UI state will break spectacularly here.

Slide Over — a third app floats over yours in a small panel. Your app goes .inactive while the Slide Over app is being interacted with.

The real reason SceneDelegate was introduced was Multiple Windows. Split View always worked. Same-app multiple windows needed the UIWindow to be per-scene rather than per-app, and that required an architectural redesign all the way up to the delegate level.


Migrating from AppDelegate to SceneDelegate

For a typical single-scene app, the migration is mechanical:

Step 1: Add UIApplicationSceneManifest to Info.plist:

<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <false/>
    <key>UISceneConfigurations</key>
    <dict>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneConfigurationName</key>
                <string>Default Configuration</string>
                <key>UISceneDelegateClassName</key>
                <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
            </dict>
        </array>
    </dict>
</dict>

Step 2: Create SceneDelegate.swift. Move window setup from AppDelegate into scene(_:willConnectTo:).

Step 3: Move UI lifecycle methods to SceneDelegate.

Step 4: Remove the old AppDelegate UI methods — they will never be called again once a Scene Manifest exists.

You do not need to implement application(_:configurationForConnecting:options:) in AppDelegate if your scene configuration is fully defined in Info.plist. That method is only required when you need to configure scenes dynamically at runtime, or when you're supporting multiple distinct scene types.


Common Interview Questions

These are the questions I've seen asked at every level, from mid-level to staff, at top-tier iOS shops.


Q: What's the difference between sceneWillEnterForeground and sceneDidBecomeActive? When would you use each?

Both fire when the app comes to the foreground, but they fire at different moments. sceneWillEnterForeground fires while the app is still .inactive — before it's fully interactive. sceneDidBecomeActive fires when it becomes fully interactive. Use sceneWillEnterForeground for things that need to be ready before the user can interact — refreshing auth tokens, removing a blur overlay. Use sceneDidBecomeActive for things that need interactivity to be meaningful — restarting timers, resuming animations, polling for new data.


Q: Where should you save user data, and why not applicationWillTerminate?

sceneDidEnterBackground is the correct and reliable save point. applicationWillTerminate is only called when the app is terminated from an active background state — which is rarely how termination actually happens. Most terminations come from the suspended state, with no callback at all. If your data integrity depends on applicationWillTerminate, you have a race condition disguised as a lifecycle method.


Q: Can an app go from Not Running directly to Background without ever being Active?

Yes. When the system wakes your app for a background task — silent push notification, background fetch, significant location change — the app launches directly into the background. didFinishLaunchingWithOptions is called, then your background handler. The app never becomes active. This is why didFinishLaunchingWithOptions must be lean and fast: it runs even when no user is looking.


Q: sceneWillResignActive — does it always lead to sceneDidEnterBackground?

No. A phone call, Control Centre swipe, or Siri activation goes Active → Inactive → Active without touching Background at all. This is an important architectural distinction. Anything you do in sceneWillResignActive must be safely undone in sceneDidBecomeActive. Don't make irreversible changes there.


Q: How is sceneDidDisconnect different from app termination?

sceneDidDisconnect means iOS released the scene from memory, but the session still exists in AppDelegate. The scene can be reconnected later. It is not termination. The correct response is to release scene-specific resources and save state for potential restoration. The wrong response — which I've seen in production code — is to delete persistent data assuming the user is done. They might not be.


Q: In a SwiftUI app, why don't you see .inactive logged on first launch when observing scenePhase?

Because onChange only fires on changes, not on the initial value. scenePhase starts as .inactive by default — that's its initial value, not a transition. The first change onChange detects is the transition from .inactive to .active. Using onChange(of:initial:true) in iOS 17+ would reveal the inactive → inactive initial fire, confirming that .inactive is indeed the starting state.


Q: You have a video player app. When would you pause playback — sceneWillResignActive or sceneDidEnterBackground?

sceneWillResignActive. By sceneDidEnterBackground, the app is already off screen. More importantly, if the user gets a phone call, the app goes inactive but never reaches background. Waiting for sceneDidEnterBackground means the video keeps playing during the call. Pausing on sceneWillResignActive is the correct UX — but whether you resume on sceneDidBecomeActive (after any interruption) or only after returning from background is a product decision. YouTube doesn't pause for notification centre pulls. A meditation app probably should.


Q: Can you observe the suspended state?

No. Suspension is a kernel-level operation. Your app's code is not running, so there is no callback to deliver. The closest you can get is inferring it: record a timestamp when entering background, compare it when returning to active. If more than N minutes passed, you were likely suspended long enough to warrant a full data refresh rather than a gentle resume.


Q: Does configurationForConnecting need to be implemented when migrating to SceneDelegate?

No, not if your scene configuration is fully defined in Info.plist. iOS reads the configuration from the plist and never needs to ask. You only need configurationForConnecting when you want to configure scenes dynamically at runtime, or when you have multiple scene types that require different SceneDelegate classes.


Q: In a SwiftUI app, you read scenePhase at both the App struct level and inside a deeply nested view. On an iPhone, are these values always the same?

Yes, on iPhone they are always identical because iPhone is always a single-scene environment. On iPadOS with multiple windows enabled, they can differ — the App-level scenePhase reflects the aggregate state of all scenes (only goes .background when every scene is backgrounded), while a View-level scenePhase reflects only the scene containing that view. A staff-level answer also notes that this distinction is a common source of bugs in iPad apps that support multiple windows.


The Mental Model, Summarised

Think of lifecycle management as having three layers of responsibility:

The process layer (AppDelegate) — I exist, I have resources, I can receive system events. Handle this once, at launch, and only for things that are truly global.

The scene layer (SceneDelegate / scenePhase) — I have a UI, users can see me, I transition between visible and invisible. This is where most of your runtime behaviour lives.

The view layer — I am a specific piece of UI. I appear, I disappear, I respond to the scene's state. Keep this layer thin. Views shouldn't need to know more about lifecycle than their immediate scenePhase or onAppear/onDisappear.

The biggest mistake I see in iOS codebases is lifecycle logic scattered across all three layers with no clear ownership. AppDelegate doing UI work. Views triggering network refreshes based on onAppear without considering scene state. Background saves missing entirely because someone assumed applicationWillTerminate was reliable.

Get the mental model right, and the method names are just implementation details.