SwiftUI's App Lifecycle Explained

Written by LearnAppMaking on March 11 2021 in App Development, SwiftUI

SwiftUI's App Lifecycle Explained

Bye-bye AppDelegate! You can now build SwiftUI apps with the new App protocol and lifecycle, without needing an app- or scene delegate. How does the SwiftUI App lifecycle work? And how do you configure it? Let’s find out!

In this tutorial, we’ll discuss:

  • How the SwiftUI App lifecycle and your App struct works
  • Working with @main, scenes and lifecycle events
  • Bootstrapping your app’s UIs with the App protocol
  • Working with UIApplicationDelegateAdaptor the “old way”

Ready? Let’s go.

  1. Quick Recap: The App Delegate
  2. What’s a SwiftUI App?
  3. Configuring Your SwiftUI App
  4. Responding to Scene Lifecycle Events
  5. Working with UIApplicationDelegateAdaptor
  6. Further Reading

Quick Recap: The App Delegate

Before we get to the App struct, let’s back up a bit.

Every app and computer program has a starting point. Think of it as the first function of your app that’s called by the Operating System (like iOS). On most platforms, this function is called main().

Prior to iOS 14, most iOS apps had an AppDelegate class (and optionally a SceneDelegate) in their Xcode project. This app delegate is annotated with the @UIApplicationMain keyword, indicating that this app delegate is the starting point of the app.

Technically, iOS creates an instance of UIApplication and assigns an instance of your app delegate class to its delegate property. Because of delegation, you can now customize what happens when the app starts, hook into lifecycle events, and configure “routes” to start your app from, like custom URLs and local/remote notifications.

An app delegate’s most important function is application(_:didFinishLaunchingWithOptions:). Ironically, this function almost always returns true. It’s an iOS app’s “main” function, the first one called during the lifetime of the app.

You typically use it for:

  • Initial configuration of 3rd-party components and frameworks, such as setting up Firebase, Bugsnag, Realm, OneSignal, Parse Server, Reachability services, and so on
  • If you’re not using a scene delegate, you’d set up the app window’s root view controller here – the “first” UI of the app (otherwise, you’d do that in the scene delegate or Info.plist)
  • Changing your app’s global appearance using appearance proxies
  • Responding to incoming remote or local notifications, i.e. a push notification comes in, and you open a specific UI in the app, as opposed to the usual/main UI

The App Delegate is also used for hooking into lifecycle events. This is exactly what the word “lifecycle” implies: the app starts, becomes active, the user takes some action, and a while later, the app is minimized and moves to the background, and is eventually terminated.

Depending on your app, lifecycles aren’t important at all – or they’re very important. For example, an iOS game will need to pause when you get a phone call and the app becomes inactive for a while, until you resume playing. Resource-intensive apps will need to pause timers and/or background processes or, in fact, start a background process when you close the app. All that happens in the App Delegate!

But… what about the SwiftUI App lifecycle?

What’s a SwiftUI App?

iOS 14 introduced the App protocol, for what many consider “SwiftUI 2.0”. In essence, the App protocol replaces the app delegate, and takes over many of its functions. Your app is now a “pure” SwiftUI App; it doesn’t have that App Delegate anymore.

Here’s what that looks like for a simple app:

@main
struct BooksApp: App
{
    var body: some Scene {
        WindowGroup {
            BookList()
        }
    }
}

Awesome! Quite concise, right? The above code will bootstrap your entire app, setting it up with an initial view called BookList.

Here’s what’s going on:

  • The @main attribute, declared for the BooksApp struct, indicates that this struct “contains the top-level entry point for program flow”. That’s a fancy way of saying that the App protocol has a default implementation for a static function main(), that’ll initialize the app when called.
  • The struct BooksApp: App { ··· code declares a struct called BooksApp, which adopts the protocol App. You can name this struct anything you want, but it’s common to choose the name of your app plus “~App”, like BooksApp.
  • The var body: some Scene { ··· code declares the required body property for the App protocol, which is incredibly similar to a SwiftUI view’s body property. Its type is Scene (not View!), and thanks to the some keyword, the concrete type of body depends on its implementation. This is boilerplate code; consider simply that “an app has a body that provides a (first) scene for the app”.
  • Inside the body property sits a WindowGroup instance. This is a cross-platform struct that represents a scene of multiple windows. You can use it on macOS, iOS, etc. It’s the container for your apps view hierarchy, kinda like the good ol’ UIWindow.
  • Within the WindowGroup, you declare the first view (“User Interface”) for your app. In the above code, that’s a BookList view – but it can be, of course, anything.

You can’t help but notice the similarities between the SwiftUI View protocol, and the App protocol. Working with a SwiftUI App feels familiar!

In fact, you may have noticed that UIKit’s preference for delegation has been replaced by SwiftUI’s preference for composability, protocols, and default protocol implementations. It’s the same, but different.

How do you get started with a SwiftUI App? Create a new app project in Xcode, and choose SwiftUI App for Lifecycle in the setup wizard. That’s it!

There’s no unified name for the SwiftUI App. As you’ll soon see, from a code point-of-view, it is a struct that conforms to the new App protocol. In Xcode, it’s often called a SwiftUI App or SwiftUI App Lifecycle. Throughout this tutorial, we’ll refer to it in its many names – but probably most as “SwiftUI App” (uppercase “A”).

Configuring Your SwiftUI App

One of the responsibilities of a SwiftUI App, and previously the App Delegate, is configuring your app. You’ll want to set up the app’s environment in such a way that it works the way it is supposed to. For example, by pointing the app to a Core Data context.

Here, check this out:

@main
struct CardsApp: App
{
    let persistenceController = PersistenceController.preview

    var body: some Scene {
        WindowGroup {
            CardList()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}

The above code uses a boilerplate PersistenceController (template from Xcode) to inject an instance of NSManagedObjectContext into the CardList view. It’s a common approach to give a SwiftUI view access to a Core Data container, but that’s beyond this example.

In the above code, you see 2 important things happening:

  1. The CardsApp struct has a property persistenceController, which is initialized with a default value. When the struct is initialized, this property is too.
  2. The property is used to inject a value into the environment of the CardList view (and hierarchy) with the environment(_:_:) modifier.

In this example, the Core Data container (and persistence controller) need to be initialized before the CardList view is shown on screen. That’s why the property is added to the CardsApp struct. When an instance of this struct is initialized, so is the persistence controller – just in time before the UI is shown on screen.

Here’s another example:

@main
struct BooksApp: App {

    init() {
        Bugsnag.start()
    }

    var body: some Scene {
        ···
    }
}

In the above code, we’re initializing the Bugsnag crash reporting service. This happens in the initializer function init() of the App struct. When the app is initialized, the Bugsnag.start() function is called, which will further set up integration with the Bugsnag service.

Just as before, the exact example isn’t so important – but the approach used to set up dependencies is. If your goal is to prepare the app to run, init() is a great place to set up services, configuration options, and so on.

Responding to Scene Lifecycle Events

Now that you’ve got your app set up, you’ll want to respond to lifecycle changes. We’ve discussed them before: what happens when your app is minimized by the user, for example?

Check this out:

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

    var body: some Scene {
        WindowGroup {
            BookList()
        }
        .onChange(of: scenePhase) { phase in
            if phase == .background {
                // clean up resources, stop timers, etc.
            }
        }
    }
}

In the above code, we’re using a few core SwiftUI principles. In short, the onChange(of:perform:) modifier invokes its closure every time the “phase” of scenePhase changes. This scene phase is an environment value that’s available for both App and View instances.

The ScenePhase enum has 3 states:

  1. active – the scene is in the foreground and active
  2. inactive – the scene is in the foreground, but inactive
  3. background – the scene is currently not visible in the UI

Changing from one phase to another is a lifecycle event. It tells you something about what’s going on in the “life” of an app. For example, the phase of your app changes to inactive when you bring up the App Switcher.

Imagine we’re running the code below. In fact, give it a try yourself!

.onChange(of: scenePhase) { phase in
    print(phase)
}

This code effectively prints out every scene phase change for the app. Here’s what you’d find:

  • Starting the app: active
  • Bringing up the App Switcher: active to inactive
  • Minimizing or closing the app: background
  • Opening the app from App Switcher: inactive to active

What’s important to note here is that iOS is in control of what phase change happens when. You can’t rely on an app being active at one moment, and then always going through the inactive phase, prior to changing to background and termination of the app. Don’t rely on an exact order of phases.

Instead, use the scene phases and lifecycle events to respond in a way that’s appropriate for your app. A few examples:

  • Stop a game or an intensive computation when the phase is inactive
  • Cancel timers when the phase is inactive, and (re)start for active
  • Stop network requests and close open files for background

Last but not least: you can observe scene phases in both a View and for an App. Inside the App struct, you get updates for every scene that’s connected to the app. Inside a View, you only get updates for the scene that the view is a part of. That’s actually helpful, because you can, say, cancel a timer “deep” within the app, right inside the view that’s using that timer.

Note: The naming of the “background” phase is tricky. Think of it as “this app is about to quit”, as opposed to “this app is running in the background”. As we’ve discussed, iOS is in control of these phases. It may well be that your app appears to be running in the background, because it’s still visible in the App Switcher, whereas the app is actually terminated because the user hasn’t used it in a while.

Working with UIApplicationDelegateAdaptor

Not ready yet to let go of your beloved App Delegate? Don’t worry, you can still use it in a SwiftUI App via the @UIApplicationDelegateAdaptor property wrapper!

Check this out:

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

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

class AppDelegate: NSObject, UIApplicationDelegate
{
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool
    {
        ParseSwift.initialize(applicationId: "···", serverURL: URL(string: "http://localhost:1337/parse")!)

        return true
    }   
}

OH NO! It’s an app delegate!?

In the above code, we’ve essentially told the App struct to initialize an instance of AppDelegate, assign it to the appDelegate property, and consider that object the app delegate of our app. All that happens via the @UIApplicationDelegateAdaptor property wrapper.

You see that the class AppDelegate conforms to NSObject and UIApplicationDelegate, just as an ordinary app delegate would. This also means you can use any available delegate function, such as application(_:didFinishLaunchingWithOptions:).

You can use the UIApplicationDelegateAdaptor for any functionality that’s not yet available on the App protocol, such as remote push notifications. This works exactly the same as it always has, for now.

It’s worth noting here that although you can use the app delegate for anything, it’s smart to use it only for functionality that you cannot achieve with the App struct alone. In the above example for Parse Server, we could just as well do this:

@main
struct ParseApp: App
{
    init() {
        ParseSwift.initialize(···)
    }

    var body: some Scene ···
}

This saves us from a whole lot of boilerplate code, which is one of the most important advantages of using the SwiftUI App lifecycle!

Further Reading

The SwiftUI App structure and lifecycle is interesting, right? It’s the beginning of a new approach to bootstrap and configure “pure” SwiftUI apps. Even though the App struct doesn’t support all features that the app- and scene delegates traditionally have, it’s a great way to set up a simple SwiftUI app.

Here’s what we’ve discussed:

  • How do you set up a SwiftUI App and App struct?
  • Working with @main, scenes and lifecycle events
  • Configuring the app’s environment and services
  • Using @UIApplicationDelegateAdaptor as a fallback app delegate

Want to learn more? Check out these resources:

LearnAppMaking

LearnAppMaking

At LearnAppMaking.com, app developers learn how to build and launch awesome iOS apps.