← All lessonsLesson 6 of 57 · Part 3: Getting started with SwiftUI
Lesson 6

What runs before the first view

App structure: @main, App, Scene, WindowGroup

You will buildNothing new on the screen in this lesson. You find out what happens before ContentView appears and after the user leaves: the entry point, the scene, the phases an app moves through — and you give Trips a store that lives as long as the app does.

TimeAbout 75 minutes

WhyA UIKit app starts in an AppDelegate, gets a SceneDelegate, makes a window and sets its root view controller, and is then told about its life through a dozen callbacks. A SwiftUI app has one small file instead. In this lesson you read that file properly, watch it work in the console, and see where each thing you used to do in the delegates has gone. This is also the lesson where the most interview answers have changed in the last few years, because the app life cycle is no longer a set of callbacks: it is a value.

Step 1

Open your project

  1. Open your Trips project as lesson 5 left it: double-click Trips.xcodeproj in Finder, or use File ▸ Open… (⌘O) in Xcode.

  2. If you did not finish lesson 5, or would rather start from a known state, download Trips-lesson-05.zip instead. It is the project exactly as it stood at the end of lesson 5. Unzip it and open Trips.xcodeproj.

  3. Run it (⌘R) to check that it builds. You should see where lesson 5 ended:

    Trips, as it stood at the end of lesson 5 — screenshot
    Trips, as it stood at the end of lesson 5Screenshot · iPhone 17, iOS 27.0
Step 2

The file you have not opened yet

Click TripsApp.swift in the Project navigator. Xcode wrote it on lesson 5, and you have not needed it since:

TripsApp.swift, as Xcode made it
import SwiftUI

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

Eleven lines, and they replace two delegate classes, a storyboard reference and a scene manifest. Read them against what you know:

In UIKitHere
@UIApplicationMain on the AppDelegate class (later @main)@main on a struct that conforms to App. It marks where the program starts.
UIScene, UIWindowScene, and your SceneDelegatebody: some Scene. A scene is a piece of the interface whose life the system manages.
Making a UIWindow and setting its rootViewControllerWindowGroup { ContentView() }: a window, and the view at its root.
application(_:didFinishLaunchingWithOptions:)Mostly nothing. For work at launch, an init() in this struct.
sceneDidBecomeActive, sceneWillResignActive, sceneDidEnterBackgroundOne value in the environment, scenePhase. Step 4.

@main is not part of SwiftUI. It is a Swift attribute (5.3) that says: this type has a static main() method, and the program begins there. App provides that method for you. some Scene is the opaque type from lesson 3, and the braces of body are a builder block like lesson 4's — a scene builder this time.

Step 3

Watch the app start

The best way to learn what runs when is to make each piece say so. Give the app an initializer:

In TripsApp.swift, make these changes
import SwiftUI@mainstruct TripsApp: App {    init() {        print("TripsApp.init")    }    var body: some Scene {        WindowGroup {            ContentView()        }    }}
import SwiftUI

@main
struct TripsApp: App {
    init() {
        print("TripsApp.init")
    }

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

And in ContentView.swift, have the view announce three things: that it has been created, that its body has run, and that it is on the screen.

In ContentView.swift, replace the ContentView struct with this
struct ContentView: View {    init() {        print("ContentView.init")    }    var body: some View {        let _ = print("ContentView.body")        ScrollView {            VStack {                ForEach(Trip.samples) { trip in                    TripCard(trip: trip)                }            }        }        .onAppear {            print("ContentView appeared")        }    }}
struct ContentView: View {
    init() {
        print("ContentView.init")
    }

    var body: some View {
        let _ = print("ContentView.body")
        ScrollView {
            VStack {
                ForEach(Trip.samples) { trip in
                    TripCard(trip: trip)
                }
            }
        }
        .onAppear {
            print("ContentView appeared")
        }
    }
}

Run the app (⌘R). The screen is unchanged. Xcode's console shows

The screen has not changed — screenshot
The screen has not changedScreenshot · iPhone 17, iOS 27.0
Console
TripsApp.init
ContentView.init
ContentView.body
ContentView appeared

That is the launch of a SwiftUI app, in order. The App struct is created. Its body builds the scene, which creates the root view. SwiftUI asks the view for its body. Then the view appears. The line let _ = print(…) is the trick from lesson 4 for printing inside a builder block.

Step 4

The life cycle is a value

A UIKit app is told when it becomes active or goes to the background: methods are called on a delegate. A SwiftUI app reads it. The current phase is a value in the environment, and you can run code when it changes. Add two things to TripsApp.swift: the property that reads the phase, and an onChange on the scene.

In TripsApp.swift, make these changes
import SwiftUI@mainstruct TripsApp: App {    @Environment(\.scenePhase) private var scenePhase    init() {        print("TripsApp.init")    }    var body: some Scene {        WindowGroup {            ContentView()        }        .onChange(of: scenePhase) { old, new in            print("\(old) -> \(new)")        }    }}
import SwiftUI

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

    init() {
        print("TripsApp.init")
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { old, new in
            print("\(old) -> \(new)")
        }
    }
}

@Environment(\.scenePhase) reads a value that the system keeps up to date; the environment has a lesson of its own (16). onChange(of:) calls its closure whenever the value changes, and hands it the old value and the new one. Run the app. Then:

  1. In Device Hub, choose Controls ▸ Home — or click the Home button below the picture of the phone. The app goes away and the home screen appears.

  2. Click the Trips icon on the home screen to come back.

Xcode's console, as the app starts

The screen has not changed — screenshot
The screen has not changedScreenshot · iPhone 17, iOS 27.0
Console
TripsApp.init
background -> inactive
ContentView.init
ContentView.body
ContentView appeared
inactive -> active

When you press Home:

Console
active -> inactive
inactive -> background

And when you come back:

Console
background -> inactive
inactive -> active

There are three phases, and an app never jumps between the outer two. It always passes through inactive.

PhaseWhat it meansUIKit said
activeOn the screen, and receiving touches.sceneDidBecomeActive
inactiveOn the screen, or partly, but not receiving touches.sceneWillResignActive
backgroundNot on the screen. Apple: "expect the app to terminate soon after."sceneDidEnterBackground
Step 5

Inactive is not background

inactive is easy to forget, because pressing Home passes through it in an instant. It lasts longer than that when something covers your app without replacing it. Run the app again, and in Device Hub drag downwards from the top-right corner of the phone's screen, to pull Control Center over the app.

Control Center, pulled down over Trips — screenshot
Control Center, pulled down over TripsScreenshot · iPhone 17, iOS 27.0

The console adds one line:

Console
active -> inactive

Swipe Control Center away again, and:

Console
inactive -> active

No background at all. The same happens for the notification shade, the app switcher, a system alert, an incoming call. So the rule for what to do in each phase: stop things that need the user's attention when you leave active — a game, a timer, a video. Save when you reach background.

Step 6

A store that lives as long as the app

So far the trips are a static list of samples that ContentView reads directly. A real app has one collection of trips that every screen shares, and that is saved when the app leaves. In SwiftUI that is an @Observable class: a class whose properties a screen can watch, so that a change to one redraws every view that read it. How it does that is lesson 16; for now the one word is enough. Make a new Swift File named TripStore, and add:

Add to TripStore.swift
@Observable
final class TripStore {
    var trips = Trip.samples

    func save() {
        print("Saving \(trips.count) trips")       // a real save comes in Part 10
    }
}

The store has to be created once and kept for as long as the app runs. The App struct is the place for that. Give it the store as @State, hand it to the first view, and save when the app goes to the background:

In TripsApp.swift, make these changes
import SwiftUI@mainstruct TripsApp: App {    @Environment(\.scenePhase) private var scenePhase    @State private var store = TripStore()    init() {        print("TripsApp.init")    }    var body: some Scene {        WindowGroup {            ContentView()            ContentView(store: store)        }        .onChange(of: scenePhase) { old, new in            print("\(old) -> \(new)")            if new == .background {                store.save()            }        }    }}
import SwiftUI

@main
struct TripsApp: App {
    @Environment(\.scenePhase) private var scenePhase
    @State private var store = TripStore()

    init() {
        print("TripsApp.init")
    }

    var body: some Scene {
        WindowGroup {
            ContentView(store: store)
        }
        .onChange(of: scenePhase) { old, new in
            print("\(old) -> \(new)")
            if new == .background {
                store.save()
            }
        }
    }
}

ContentView now receives its trips instead of reaching for the samples:

In ContentView.swift, replace the ContentView struct with this
struct ContentView: View {    init() {    let store: TripStore    init(store: TripStore) {        self.store = store        print("ContentView.init")    }    var body: some View {        let _ = print("ContentView.body")        ScrollView {            VStack {                ForEach(Trip.samples) { trip in                ForEach(store.trips) { trip in                    TripCard(trip: trip)                }            }        }        .onAppear {            print("ContentView appeared")        }    }}
struct ContentView: View {
    let store: TripStore

    init(store: TripStore) {
        self.store = store
        print("ContentView.init")
    }

    var body: some View {
        let _ = print("ContentView.body")
        ScrollView {
            VStack {
                ForEach(store.trips) { trip in
                    TripCard(trip: trip)
                }
            }
        }
        .onAppear {
            print("ContentView appeared")
        }
    }
}

Xcode reports an error — in the preview, as on lesson 5

missing argument for parameter 'store' in call

The preview creates a ContentView, so it needs a store too. It can simply make one:

In ContentView.swift, replace the preview with this
#Preview {    ContentView()    ContentView(store: TripStore())}
#Preview {
    ContentView(store: TripStore())
}

Run the app, press Home (Controls ▸ Home), and read the console:

The screen has not changed — screenshot
The screen has not changedScreenshot · iPhone 17, iOS 27.0
Console
active -> inactive
ContentView.init
inactive -> background
Saving 3 trips
ContentView.init

The save is there. So is something you did not ask for: ContentView.init, after each change of phase. Scroll up in the console and you will find that ContentView.body was printed once, when the app started, and never again.

Each change of phase makes SwiftUI evaluate the App's body again, because that body reads scenePhase. Evaluating it creates a new ContentView value — so the initializer runs — and then SwiftUI finds that nothing the view shows has changed, and does not ask for its body. The view struct is made over and over. The view on the screen is left alone. (In step 4 the struct was not even made again: there, the view was created without being given anything.) Hold on to this. It is the whole of the next lesson.

Three details in the code you changed.

In the codeWhat it means
@State private var store = TripStore()The app owns one store. @State keeps it alive for as long as the app runs, and creates it exactly once — one of lesson 12's edge cases shows that this is new in Xcode 27.
let store: TripStore in the viewNo wrapper is needed to read an @Observable object. SwiftUI tracks which of its properties body reads, and runs body again when one of them changes. On lesson 16 you write that tracking by hand.
if new == .backgroundThe one moment the system promises before your app may be suspended or ended. There is no "will terminate" to rely on.
One store, many windows

On iPhone a WindowGroup is one window. On iPad and Mac the user can open several from the same group. Apple: "Every window in the group maintains independent state." So @State inside ContentView would exist once per window — and the store, which belongs to the App, is shared by all of them. That is the reason to put app-wide data here and not in the first view.

Step 7

When you still need an app delegate

Some things still arrive only through a UIKit app delegate: the device token for push notifications is the usual example, and some third-party libraries ask for one. SwiftUI lets you attach a delegate without giving up App. Try it once, to see the order of events. Make a new Swift File named TripsDelegate, and replace everything in it:

New fileTripsDelegate.swift — the whole file
import UIKit

final class TripsDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        print("TripsDelegate: did finish launching")
        return true
    }
}

And in TripsApp.swift, one new property — the first line inside the struct:

In TripsApp.swift, make these changes
import SwiftUI@mainstruct TripsApp: App {    @UIApplicationDelegateAdaptor private var delegate: TripsDelegate    @Environment(\.scenePhase) private var scenePhase    @State private var store = TripStore()    init() {        print("TripsApp.init")    }    var body: some Scene {        WindowGroup {            ContentView(store: store)        }        .onChange(of: scenePhase) { old, new in            print("\(old) -> \(new)")            if new == .background {                store.save()            }        }    }}
import SwiftUI

@main
struct TripsApp: App {
    @UIApplicationDelegateAdaptor private var delegate: TripsDelegate
    @Environment(\.scenePhase) private var scenePhase
    @State private var store = TripStore()

    init() {
        print("TripsApp.init")
    }

    var body: some Scene {
        WindowGroup {
            ContentView(store: store)
        }
        .onChange(of: scenePhase) { old, new in
            print("\(old) -> \(new)")
            if new == .background {
                store.save()
            }
        }
    }
}

Run the app. The console begins

The screen has not changed — screenshot
The screen has not changedScreenshot · iPhone 17, iOS 27.0
Console
TripsApp.init
TripsDelegate: did finish launching
background -> inactive
ContentView.init
ContentView.body
ContentView appeared
inactive -> active
ContentView.init

Your App is created first. SwiftUI then creates your delegate and passes the launch on to it. Only after that does the scene, and with it the first view, come into being. You never create the delegate yourself, and UIApplication's own delegate is still SwiftUI's — yours is being forwarded to.

Apple's advice is to avoid this when you can: "Manage an app's life cycle events without using an app delegate whenever possible." Trips does not need one. Take it out again: delete TripsDelegate.swift (select it in the Project navigator, press the Delete key, and choose Move to Trash), and remove the property from TripsApp.swift:

In TripsApp.swift, make these changes
import SwiftUI@mainstruct TripsApp: App {    @UIApplicationDelegateAdaptor private var delegate: TripsDelegate    @Environment(\.scenePhase) private var scenePhase    @State private var store = TripStore()    init() {        print("TripsApp.init")    }    var body: some Scene {        WindowGroup {            ContentView(store: store)        }        .onChange(of: scenePhase) { old, new in            print("\(old) -> \(new)")            if new == .background {                store.save()            }        }    }}
import SwiftUI

@main
struct TripsApp: App {
    @Environment(\.scenePhase) private var scenePhase
    @State private var store = TripStore()

    init() {
        print("TripsApp.init")
    }

    var body: some Scene {
        WindowGroup {
            ContentView(store: store)
        }
        .onChange(of: scenePhase) { old, new in
            print("\(old) -> \(new)")
            if new == .background {
                store.save()
            }
        }
    }
}
Step 8

Only one way in

A program has one entry point. See what the compiler says about a second. Add this at the bottom of TripsApp.swift:

Add to TripsApp.swift
@main
struct OtherApp: App {
    var body: some Scene {
        WindowGroup {
            Text("Another way in")
        }
    }
}

It does not compile. Xcode reports, on both structs

'main' attribute can only apply to one type in a module

Delete the second struct again.

In TripsApp.swift, delete the OtherApp struct
@mainstruct OtherApp: App {    var body: some Scene {        WindowGroup {            Text("Another way in")        }    }}
Step 9

Tidy up

The prints have done their work. Leave the app as it should be: the store, and the save. In TripsApp.swift:

In TripsApp.swift, make these changes
import SwiftUI@mainstruct TripsApp: App {    @Environment(\.scenePhase) private var scenePhase    @State private var store = TripStore()    init() {        print("TripsApp.init")    }    var body: some Scene {        WindowGroup {            ContentView(store: store)        }        .onChange(of: scenePhase) { old, new in            print("\(old) -> \(new)")        .onChange(of: scenePhase) { _, new in            if new == .background {                store.save()            }        }    }}
import SwiftUI

@main
struct TripsApp: App {
    @Environment(\.scenePhase) private var scenePhase
    @State private var store = TripStore()

    var body: some Scene {
        WindowGroup {
            ContentView(store: store)
        }
        .onChange(of: scenePhase) { _, new in
            if new == .background {
                store.save()
            }
        }
    }
}

{ _, new in ignores the old value, which the code no longer uses. And in ContentView.swift:

In ContentView.swift, replace the ContentView struct with this
struct ContentView: View {    let store: TripStore    init(store: TripStore) {        self.store = store        print("ContentView.init")    }    var body: some View {        let _ = print("ContentView.body")        ScrollView {            VStack {                ForEach(store.trips) { trip in                    TripCard(trip: trip)                }            }        }        .onAppear {            print("ContentView appeared")        }    }}
struct ContentView: View {
    let store: TripStore

    var body: some View {
        ScrollView {
            VStack {
                ForEach(store.trips) { trip in
                    TripCard(trip: trip)
                }
            }
        }
    }
}

Without an init of your own, Swift writes one for the struct that takes the store, so ContentView(store: store) still works. Run it one last time and press Home:

Run it (⌘R). The simulator shows

Trips at the end of lesson 6: the same screen, drawn from the store — screenshot
Trips at the end of lesson 6: the same screen, drawn from the storeScreenshot · iPhone 17, iOS 27.0

And in the console, when you press Home, one line:

Console
Saving 3 trips

What you built

Next: the puzzle from step 6. ContentView.init ran again and again, and ContentView.body only once. What a view really is, when body runs, and why none of your setup code belongs in a view's init.

The finished project

Stuck, or want to compare with your own? This is the project as it stands at the end of this lesson, built and run before it was put here.

Flash cards

13 cards for this lesson, each labelled with its topic. 7 ask how you would write something; the other 6 are trick questions, written to catch a misunderstanding. Answer in your head first, then tap the card.

Syntax · App structureWrite the entry point of a SwiftUI app whose first screen is ContentView.
import SwiftUI

@main
struct TripsApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

A struct that conforms to App, marked @main. Its body returns scenes; WindowGroup is the scene for an ordinary window, and its braces hold the root view.

Syntax · App structureRun some code once, as the app starts — before any view exists.
import SwiftUI

@main
struct TripsApp: App {
    init() {
        print("starting")
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

Give the App struct an init(). It is the earliest of your own code to run: it printed starting before the first view was created.

Syntax · Scene phaseDo something when the app goes to the background.
import SwiftUI

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

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { old, new in
            if new == .background {
                print("save now")
            }
        }
    }
}
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

Read \.scenePhase from the environment, and attach onChange(of: scenePhase) to the scene. The phase is .active, .inactive or .background.

Syntax · Scene phaseIn a view, pause something whenever the app is not active.
import SwiftUI

struct ContentView: View {
    @Environment(\.scenePhase) private var scenePhase
    @State private var isRunning = true

    var body: some View {
        Text(isRunning ? "Running" : "Paused")
            .onChange(of: scenePhase) {
                isRunning = (scenePhase == .active)
            }
    }
}
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

A view can read \.scenePhase too. onChange also has a form whose closure takes no parameters; inside it, the property already holds the new value.

Syntax · App structureKeep one model object for the whole life of the app, and hand it to the first view.
import SwiftUI

@main
struct TripsApp: App {
    @State private var store = TripStore()

    var body: some Scene {
        WindowGroup {
            ContentView(store: store)
        }
    }
}
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

@State in the App struct. The app owns the object; every window made from the WindowGroup is given the same one.

Syntax · App structureAttach a UIKit app delegate to a SwiftUI app.
import SwiftUI

final class TripsDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        print("token:", deviceToken.count, "bytes")
    }
}

@main
struct TripsApp: App {
    @UIApplicationDelegateAdaptor private var delegate: TripsDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

@UIApplicationDelegateAdaptor, with the delegate's type. SwiftUI creates the delegate and forwards the app's callbacks to it. Declare it once, in the App struct only.

Syntax · Scene phaseRun code when a value changes, with both the old value and the new one.
Stepper("\(nights) nights", value: $nights)
    .onChange(of: nights) { old, new in
        print("from \(old) to \(new)")
    }
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

onChange(of:) { old, new in … }. It works for any Equatable value, on a view or on a scene. Add initial: true to run it once at the start as well.

Trick question · App structureCan an app have two types marked @main?
import SwiftUI

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

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

No. 'main' attribute can only apply to one type in a module A program starts in exactly one place.

Trick question · Scene phaseThe user presses Home. Does the phase go straight from active to background?
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

No. It passes through inactive. After the launch, pressing Home printed active -> inactive and then inactive -> background. Coming back is the same in reverse.

Trick question · Scene phaseThe user pulls Control Center down over your app. Does the app go to the background?

No. It becomes inactive and stays there until Control Center is put away; then it is active again. The same goes for the notification shade, the app switcher and system alerts. background means the app is no longer on the screen at all.

Trick question · App structureIn a SwiftUI app with a @UIApplicationDelegateAdaptor, which runs first: the App struct's init, or the delegate's didFinishLaunching?
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

The App's init. It printed App.init, then delegate: did finish launching. SwiftUI creates your App, then the delegate, and then tells the delegate about the launch.

Trick question · App structureOn iPad the user opens a second window of your app. ContentView has a @State property. Do the two windows share its value?

No. Each window made from a WindowGroup gets its own copy of the view hierarchy, and its own storage for every @State in it. Apple: "Every window in the group maintains independent state." Data that all windows should share belongs to the App struct, or to a model it owns.

Trick question · App structureAppDelegate and SceneDelegate are gone from the project. Are they gone from the running app?
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

No. A SwiftUI app is still a UIKit app underneath: UIApplication has a delegate, and each scene has one. SwiftUI supplies them. Asked for their types, the running app printed SwiftUI.AppDelegate and SwiftUI.AppSceneDelegate.

Edge cases and tricky questions

9 questions of the kind an interviewer uses to find out whether you really know this — the corners, the traps, and the things that changed recently. Each has its source. Wherever the answer is what some code does, that code was compiled and run, and what you see under “Show the answer” is what it really did. Say your answer aloud before you open it.

Lesson 6 · question 1 of 9 · the phases at launch

The app and its first view both print every change of scenePhase. The app is launched, and nothing else happens. What is printed?

import SwiftUI

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

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { old, new in
            print("app:  \(old) -> \(new)")
        }
    }
}

struct ContentView: View {
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        Text("Trips")
            .onChange(of: scenePhase) { old, new in
                print("view: \(old) -> \(new)")
            }
    }
}
Show the answer
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

The console shows

Console
app:  background -> inactive
app:  inactive -> active
view: inactive -> active

Three lines, and the first surprises most people: the app's phase starts at background. A launch is a move from the background to the foreground, by way of inactive.

The view prints only the last step. By the time the view exists and is watching, the scene is already inactive, so the first change it can see is the one to active.

Two practical points. onChange on the scene fires during launch, so code there that means "the user came back" also runs when the user arrives for the first time — which is usually what you want, but know it. And the phase background at launch is not a quirk of the simulator: an app can be launched into the background by the system, for a background task or a notification, and then it never becomes active at all.

Where this comes from: ScenePhase · App organization — Apple: "The system moves your app’s Scene instances through phases that reflect a scene’s operational state."

Lesson 6 · question 2 of 9 · leaving and coming back

The same app. The user presses Home, then taps the icon to return. What is printed for each — and which line is your last dependable chance to save?

import SwiftUI

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

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { old, new in
            print("app:  \(old) -> \(new)")
        }
    }
}

struct ContentView: View {
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        Text("Trips")
            .onChange(of: scenePhase) { old, new in
                print("view: \(old) -> \(new)")
            }
    }
}
Show the answer
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

Pressing Home printed:

Console
app:  active -> inactive
view: active -> inactive
app:  inactive -> background
view: inactive -> background

And coming back:

Console
app:  background -> inactive
view: background -> inactive
app:  inactive -> active
view: inactive -> active

Always through inactive, in both directions, and the app is told before the view each time.

The line to save on is inactive -> background. Apple's documentation is blunt about what follows: "When an app enters the background phase, expect the app to terminate soon after." There is no later moment you can count on. A suspended app that the system removes from memory is not told anything, and SwiftUI has no equivalent of applicationWillTerminate.

Do not save on active -> inactive instead. That fires for Control Center, the app switcher and every system alert, so you would be saving constantly, and for the wrong reason.

Where this comes from: ScenePhase — Apple, ScenePhase: the app-level value shows background when all scenes are in the background; "you can use that opportunity to free any resources".

Lesson 6 · question 3 of 9 · the phase in an App and in a view are different values

scenePhase can be read in the App struct and in any view. On an iPad with two windows of your app side by side, the user taps in one of them. Do the two places report the same thing?

Show the answer

No, and the difference is documented. Read inside a view, scenePhase is the phase of the scene that contains that view. Read in the App, it is a summary of all scenes: Apple says it is active "if any scene is active" and inactive "when no scenes are active".

So with two windows, each view's value follows its own window, while the app's value stays active as long as either is. It reaches background only when every window has.

That tells you where each kind of work belongs. Work that concerns one window — pausing its video, stopping its timer — reads the phase in that window's views. Work that concerns the whole app — saving the store — reads it in the App. On an iPhone, with its one window, the two always agree, which is why code that confuses them survives until someone opens a second window on an iPad.

Where this comes from: ScenePhase · WindowGroup — Apple, ScenePhase: "How you interpret the value depends on where it’s read from."

Lesson 6 · question 4 of 9 · `inactive` without `background`

A game pauses itself when the phase becomes background. A player pulls down Control Center to change the volume, and loses a life while it is open. What went wrong?

Show the answer
import SwiftUI

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

    var body: some Scene {
        WindowGroup {
            Text("Trips")
        }
        .onChange(of: scenePhase) { old, new in
            print("\(old) -> \(new)")
        }
    }
}

Control Center does not send an app to the background. With it pulled down over the app, the running app printed only this:

Console
active -> inactive
Screenshot of this code running
Control Center over the app: the app is inactive, not in the backgroundScreenshot · iPhone 17, iOS 27.0

And when it was swiped away:

Console
inactive -> active

The app was still on the screen, behind the panel, and still running. It was simply not receiving touches. That is what inactive means, and it is the phase the game should have paused on.

The rule: anything that needs the user's attention stops when the phase is no longer active. Saving, and giving up resources, wait for background.

Where this comes from: ScenePhase — Apple describes inactive as a scene that is in the foreground but "should pause its work".

Lesson 6 · question 5 of 9 · `@main` and top-level code

A colleague adds a file named main.swift to the app, with one line in it, to "run something first". The project stops compiling. Why?

// main.swift
import Foundation

print("starting")

// TripsApp.swift
import SwiftUI

@main
struct TripsApp: App {
    var body: some Scene {
        WindowGroup {
            Text("Trips")
        }
    }
}
Show the answer

It does not compile. Xcode reports

'main' attribute cannot be used in a module that contains top-level code

'main' attribute cannot be used in a module that contains top-level code Swift has two ways to say where a program starts, and a module may use only one. Code written at the top level of a file named main.swift is the old way: the program starts at its first line. @main on a type is the new way: the program starts by calling that type's static main().

The proposal that introduced @main spells out the corner your colleague hit: "A main.swift file is always considered to be an entry point, even if it has no top-level code." So even an empty file with that name is enough to break the build.

Rename the file, and put the line where code that must run first goes in a SwiftUI app: the init() of the App struct.

Where this comes from: SE-0281: @main: Type-Based Program Entry Points · Hacking with Swift interview question: What does the @main attribute do? — SE-0281, “@main: Type-Based Program Entry Points” (Swift 5.3).

Lesson 6 · question 6 of 9 · what `@main` needs

@main is usually written on the App struct. Here it is on an enum that is not an App at all. Does this compile — and what is it for?

import SwiftUI

struct TripsApp: App {
    var body: some Scene {
        WindowGroup {
            Text("Trips")
        }
    }
}

struct TestingApp: App {
    var body: some Scene {
        WindowGroup {
            Text("Running tests")
        }
    }
}

@main
enum Launcher {
    static func main() {
        if NSClassFromString("XCTestCase") != nil {
            print("starting the stand-in app, for tests")
            TestingApp.main()
        } else {
            print("starting Trips")
            TripsApp.main()
        }
    }
}
Show the answer
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

The console shows

Console
starting Trips

It compiles, and it ran: the console shows which branch was taken. @main asks for one thing only — a type with a static method called main(). Hacking with Swift's interview answer puts it this way: with @main you "need to implement a main() method to handle setting up your program, but if you're using SwiftUI the App protocol provides that for you".

Because App gives every conforming type a main(), you can call it yourself, and that makes this pattern possible. Unit tests for an app run inside the app, so by default every test run starts the whole thing: the real store, the real network calls, the real first screen. A launcher like this one starts a bare stand-in app when it sees that tests are loaded, and the tests run faster and touch nothing.

If the type has no main(), the compiler tells you exactly what it wants: a static function of type () -> Void, or the throwing or async forms of it.

Where this comes from: Hacking with Swift interview question: What does the @main attribute do? · SE-0281: @main: Type-Based Program Entry Points · App — Apple, App: "The protocol provides a default implementation of the main() method that the system calls to launch your app."

Lesson 6 · question 7 of 9 · the old `onChange`

A tutorial from 2022 reacts to the scene phase like this. What does Xcode 27 say about it?

import SwiftUI

struct ContentView: View {
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        Text("Trips")
            .onChange(of: scenePhase) { newPhase in
                if newPhase == .background {
                    print("save")
                }
            }
    }
}
Show the answer
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

It compiles, with a warning

'onChange(of:perform:)' was deprecated in iOS 17.0: Use `onChange` with a two or zero parameter action closure instead.

It compiles, with a warning: 'onChange(of:perform:)' was deprecated in iOS 17.0: Use onChange with a two or zero parameter action closure instead.

The first onChange passed its closure one value, the new one. That form has been deprecated since iOS 17. The replacements take either two parameters, the old value and the new, or none at all:

import SwiftUI

struct ContentView: View {
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        Text("Trips")
            .onChange(of: scenePhase) { old, new in
                if new == .background {
                    print("save")
                }
            }
    }
}
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

The difference is more than the parameter count, which is why Apple did not simply add a parameter. The old closure ran before the view was updated, so reading the property inside it gave the old value while the parameter held the new one. The new forms run after the update: inside the closure, the property and new agree. Code that relied on the old timing has to be read again when you convert it. And onChange(of:initial:) with initial: true also runs once when the view first appears, which the old form could not do.

Where this comes from: onChange(of:initial:_:) — The compiler's own deprecation message, and Apple's reference for onChange(of:initial:_:).

Lesson 6 · question 8 of 9 · windows and state

ContentView keeps the selected trip in @State. The App keeps the store in @State. On an iPad the user opens a second window of the app. How many selections are there now, and how many stores?

Show the answer

Two selections, one store. A WindowGroup is a template: each window the user opens is a fresh copy of the view hierarchy inside its braces. Apple: "Every window in the group maintains independent state. For example, the system allocates new storage for any State or StateObject variables instantiated by the scene's view hierarchy for each window that it creates."

The App struct is outside that template. There is one of it, so there is one store, and both windows are handed the same object. Add a trip in one window and it appears in the other; select a trip in one and the other does not move. That is nearly always the behavior you want, and it falls out of putting each piece of state at the right level.

One more fact for the interview. An iPad app only offers a second window if it says it supports them: the Info.plist key is UIApplicationSupportsMultipleScenes, inside UIApplicationSceneManifest. A new Xcode project sets it for you.

Where this comes from: WindowGroup · Scene — Apple, WindowGroup.

Lesson 6 · question 9 of 9 · where the launch code went

In UIKit, set-up code went in application(_:didFinishLaunchingWithOptions:). An interviewer asks where it goes in a SwiftUI app — and whether the app delegate has really gone.

import SwiftUI

final class TripsDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        print("2. the delegate hears that launching has finished")
        print("   UIApplication's delegate is", String(reflecting: type(of: application.delegate!)))
        return true
    }
}

@main
struct TripsApp: App {
    @UIApplicationDelegateAdaptor private var delegate: TripsDelegate

    init() {
        print("1. the App struct is created")
    }

    var body: some Scene {
        WindowGroup {
            Text("Trips")
                .onAppear { print("3. the first view appears") }
        }
    }
}
Show the answer
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

The console shows

Console
1. the App struct is created
2. the delegate hears that launching has finished
   UIApplication's delegate is SwiftUI.AppDelegate
3. the first view appears

Most of it goes in the init() of the App struct, which runs first of all — the console above shows the order. Work that belongs to a model goes in that model, created by the App as @State.

The delegate has not gone; it has been hidden. The running app says that UIApplication's delegate is a class of SwiftUI's own. When you need to receive something that only a delegate is sent — the push-notification token, a background URL session's events, a callback some SDK insists on — you write a delegate class and attach it with @UIApplicationDelegateAdaptor. SwiftUI creates it and forwards to it.

Three rules come with the adaptor, all from Apple's documentation. Declare it only in the App, and only once: a second one is "a runtime error". Do not create the delegate yourself. And prefer not to need it: "Manage an app's life cycle events without using an app delegate whenever possible. For example, prefer to handle changes in ScenePhase instead of relying on delegate callbacks."

Where this comes from: UIApplicationDelegateAdaptor · App — Apple, UIApplicationDelegateAdaptor.

Further reading