The Ultimate Guide to the SwiftUI 2 Application Life Cycle
For the longest time, iOS developers have used
AppDelegate as the main entry point for their applications. With the launch of SwiftUI2 at WWDC 2020, Apple has introduced a new application life cycle that (almost) completely does away with
AppDelegate, making way for a DSL-like approach.
In this article, I’ll discuss why this change was introduced and how you can make use of the new life cycle in new or existing apps.
Specifying the Application Entry Point
One of the first questions we need to answer is “How can we tell the compiler about the entry point to our application?” SE-0281 specifies how type-based program entry points work:
“The Swift compiler will recognize a type annotated with the
@mainattribute as providing the entry point for a program. Types marked with
@mainhave a single implicit requirement: declaring a static
When creating a new SwiftUI app, the app’s
@main class looks like this:
So where is the static
main() function that’s mentioned in SE-0281?
Well, it turns out that framework providers can (and should) provide a default implementation for their users’ convenience. Looking at the code snippet above, you’ll notice that
SwiftUIAppLifeCycleApp conforms to the
App protocol. Apple provides a protocol extension that looks like this:
And there we have it — this protocol extension provides a default implementation that takes care of the application startup.
Since the SwiftUI framework isn’t open source, we can’t see how Apple implemented this, but Swift Argument Parser is open source and uses this approach as well. Check out the source code for
ParsableCommand to see how they use a protocol extension to provide a default implementation of the static
main function that serves as the program entry point:
If all this sounds a bit complicated, the good news is you don’t actually have to worry about it when creating a new SwiftUI application: Just make sure to select “SwiftUI App” in the Life Cycle drop-down when creating your app, and you’re done:
Let’s take a look at some common scenarios.
Initialising Resources/Your Favourite SDK or Framework
Most applications need to perform a few steps at application startup: fetching some configuration values, connecting to a database, or initialising a framework or third-party SDK.
Usually, you’d do this in your
application(_:didFinishLaunchingWithOptions:) method. As we no longer have an application delegate, we need to find other ways to initialise our application. Depending on your specific requirements, here are some strategies:
- Implement an initialiser on your main class (see the docs)
- Set initial values for stored properties (see the docs)
- Set default property values using a closure (see the docs)
If none of this meets your needs, you might need an
ApplicationDelegate after all. Read on until the end to learn how you can add one.
Handling Your Application’s Life Cycle
It’s sometimes useful to be able to know which state your application is in. For example, you might want to fetch new data as soon as your app becomes active or flush any caches once your application becomes inactive and transitions into the background.
Usually, you’d implement
applicationDidEnterBackground on your
Starting with iOS 14.0, Apple has provided a new API that allows for a more elegant and maintainable way of tracking an app’s state:
ScenePhase. Your project can have multiple scenes, but chances are you’ve got only one scene, represented by
SwiftUI tracks a scene’s state in the environment, and you can make the current value accessible to your code by fetching it using the
@Environment property wrapper and then using the
onChange(of:) modifier to listen to any changes:
It’s worth noting that you can read the phase from other locations in your app as well. When reading the phase at the top level of the app (like shown in the code snippet), you’ll get an aggregate of all of the phases in your app. A value of
.inactive means that none of the scenes in your app are active.
When reading the phase on a view, you’ll receive the value of the phase that contains the view. Keep in mind your app might contain other scenes that have other phase values at this time. For more details about scene phases, read Apple’s documentation.
Handling Deep Links
Previously, when handling deep links, you’d have to implement
application(_:open:options:) and route the incoming URL to the most appropriate handler.
This becomes a lot easier with the new app–life cycle model. You can handle incoming URLs by attaching the
onOpenURL modifier to the top-most scene in your app:
What’s really cool: You can install multiple URL handlers throughout your application — making deep linking a lot easier, as you can handle incoming links where it’s most appropriate.
If at all possible, you should use universal links (or Firebase Dynamic Links, which make use of universal links for iOS apps), as these use associated domains to create a connection between a website you own and your app — this will allow you to share data securely.
However, you can still use custom URL schemes to link to content within your app.
Either way, a simple way to trigger a deep link in your app is to use the following command on you development machine:
$ xcrun simctl openurl booted <your url>
Continuing User Activities
If your app uses
NSUserActivity to integrate with Siri, Handoff, or Spotlight, you need to handle user-activity continuation.
Again, the new application–life cycle model makes this easier by providing two modifiers that allow you to advertise an activity and later continue it.
Here’s a snippet that shows how to advertise an activity, for example, in a details view:
To allow continuation of this activity, you can register a
onContinueUserActivity closure in your top-level navigation view, like this:
Help — None of the Above Works for Me!
Not all of
AppDelegate’s callbacks are supported by the new application life cycle (yet). If none of the above meets your needs, you might require an
AppDelegate after all.
Another reason you might require an
AppDelegate is if you use any third-party SDKs that make use of method swizzling to inject themselves into the application life cycle. Firebase is a well-known case.
To help you out, Swift provides a way to connect a conformer of
AppDelegate with your
@UIApplicationDelegateAdaptor. Here’s how to use it:
Don’t forget to remove the
@main attribute if you copy an existing
AppDelegate implementation — otherwise, the compiler will complain about multiple application entry points.
With all this, let’s discuss why Apple made this change. I think there are a couple of reasons:
SE-0281 explicitly states that one of the design goals was “to offer a more general purpose and lightweight mechanism for delegating a program’s entry point to a designated type.”
The DSL-based approach Apple chose for handling the application life cycle aligns nicely with the declarative approach for building UIs in SwiftUI. Using the same concepts makes things easier to understand and will help onboard new developers.
Overall, the new application–life cycle model makes implementing your application startup easier and less convoluted.
The key benefit of any declarative approach is: Instead of putting the burden of implementing a specific functionality onto the developers, the framework/platform provider takes care of this. Should any changes become necessary, it’ll be a lot easier to ship these without breaking many developers’ apps — ideally, developers won’t have to change their implementation, as the framework will take care of everything for you.
Overall, the new application–life cycle model makes implementing your application startup easier and less convoluted. Your code will be cleaner and easier to maintain — and that’s always a good thing, if you ask me.
I hope this article helped you understand the ins and outs of the new application life cycle.
Thanks for reading!
If you want to learn more, check out these resources: