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:
App
struct works@main
, scenes and lifecycle eventsApp
protocolUIApplicationDelegateAdaptor
the “old way”Ready? Let’s go.
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 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:
Info.plist
)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?
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:
@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.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.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”.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
.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”).
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:
CardsApp
struct has a property persistenceController
, which is initialized with a default value. When the struct is initialized, this property is too.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.
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:
active
– the scene is in the foreground and activeinactive
– the scene is in the foreground, but inactivebackground
– the scene is currently not visible in the UIChanging 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:
active
active
to inactive
background
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:
inactive
inactive
, and (re)start for active
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.
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!
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:
App
struct?@main
, scenes and lifecycle events@UIApplicationDelegateAdaptor
as a fallback app delegateWant to learn more? Check out these resources: