Understanding the iOS 13 Scene Delegate
Published on: October 28, 2019When you create a new project in Xcode 11, you might notice something that you haven’t seen before. Instead of only creating an AppDelegate.swift
file, a ViewController.swift
, a storyboard and some other files, Xcode now creates a new file for you; the SceneDelegate.swift
file. If you’ve never seen this file before, it might be quite confusing to understand what it is, and how you are supposed to use this new scene delegate in your app.
By the end of this week's blog post you will know:
- What the scene delegate is used for.
- How you can effectively implement your scene delegate.
- Why the scene delegate is an important part of iOS 13.
Let’s jump right in, shall we?
Examining the new Xcode project template
Whenever you create a new Xcode project, you have the option to choose whether you want to use SwiftUI or Storyboards. Regardless of your choice here, Xcode will generate a new kind of project template for you to build upon. We’ll take a closer look at the SceneDelegate.swift
and AppDelegate.swift
files in the next section, what’s important for now is that you understand that Xcode has created these files for you.
In addition to these two delegate files, Xcode does something a little bit more subtle. Take a close look at your Info.plist
file. You should see a new key called Application Scene Manifest
with contents similar to the following image:
This scene manifest specifies a name and a delegate class for your scene. Note that these properties belong to an array (Application Session Role
), suggesting that you can have multiple configurations in your Info.plist
. A much more important key that you may have already spotted in the screenshot above is Enable Multiple Windows
. This property is set to NO
by default. Setting this property to YES
will allow users to open multiple windows of your application on iPadOS (or even on macOS). Being able to run multiple windows of an iOS application side by side is a huge difference from the single window environment we’ve worked with until now, and the ability to have multiple windows is the entire reason our app’s lifecycle is now maintained in two places rather than one.
Let’s take a closer look at the AppDelegate
and SceneDelegate
to better understand how these two delegates work together to enable support for multiple windows.
Understanding the roles of AppDelegate and SceneDelegate
If you’ve built apps prior to iOS 13, you probably know your AppDelegate
as the one place that does pretty much everything related to your application’s launch, foregrounding, backgrounding and then some. In iOS 13, Apple has moved some of the AppDelegate
responsibilities to the SceneDelegate
. Let’s take a brief look at each of these two files.
AppDelegate’s responsibilities
The AppDelegate
is still the main point of entry for an application in iOS 13. Apple calls AppDelegate
methods for several application level lifecycle events. In Apple’s default template you’ll find three methods that Apple considers to be important for you to use:
func application(_:didFinishLaunchingWithOptions:) -> Bool
func application(_:configurationForConnecting:options:) -> UISceneConfiguration
func application(_:didDiscardSceneSessions:)
These methods have some commentary in them that actually describes what they do in enough detail to understand what they do. But let’s go over them quickly anyway.
When your application is just launched, func application(_:didFinishLaunchingWithOptions:) -> Bool
is called. This method is used to perform application setup. In iOS 12 or earlier, you might have used this method to create and configure a UIWindow
object and assigned a UIViewController
instance to the window to make it appear.
If your app is using scenes, your AppDelegate
is no longer responsible for doing this. Since your application can now have multiple windows, or UISceneSession
s active, it doesn’t make much sense to manage a single-window object in the AppDelegate
.
The func application(_:configurationForConnecting:options:) -> UISceneConfiguration
is called whenever your application is expected to supply a new scene, or window for iOS to display. Note that this method is not called when your app launches initially, it’s only called to obtain and create new scenes. We’ll take a deeper look at creating and managing multiple scenes in a later blog post.
The last method in the AppDelegate
template is func application(_:didDiscardSceneSessions:)
. This method is called whenever a user discards a scene, for example by swiping it away in the multitasking window or if you do so programmatically. If your app isn’t running when the user does this, this method will be called for every discarded scene shortly after func application(_:didFinishLaunchingWithOptions:) -> Bool
is called.
In addition to these default methods, your AppDelegate
can still be used to open URLs, catch memory warnings, detect when your app will terminate, whether the device’s clock changed significantly, detect when a user has registered for remote notifications and more.
Tip:
It’s important to note that if you’re currently usingAppDelegate
to manage your app’s status bar appearance, you might have to make some changes in iOS 13. Several status bar related methods have been deprecated in iOS 13.
Now that we have a better picture of what the new responsibilities of your AppDelegate
are, let’s have a look at the new SceneDelegate
.
SceneDelegate’s responsibilities
When you consider the AppDelegate
to be the object that’s responsible for your application’s lifecycle, the SceneDelegate
is responsible for what’s shown on the screen; the scenes or windows. Before we continue, let’s establish some scene related vocabulary because not every term means what you might think it means.
When you’re dealing with scenes, what looks like a window to your user is actually called a UIScene
which is managed by a UISceneSession
. So when we refer to windows, we are really referring to UISceneSession
objects. I will try to stick to this terminology as much as possible throughout the course of this blog post.
Now that we’re on the same page, let’s look at the SceneDelegate.swift
file that Xcode created when it created our project.
There are several methods in the SceneDelegate.swift
file by default:
scene(_:willConnectTo:options:)
sceneDidDisconnect(_:)
sceneDidBecomeActive(_:)
sceneWillResignActive(_:)
sceneWillEnterForeground(_:)
sceneDidEnterBackground(_:)
These methods should look very familiar to you if you’re familiar with the AppDelegate
that existed prior to iOS 13. Let’s have a look at scene(_:willConnectTo:options:)
first, this method probably looks least familiar to your and it’s the first method called in the lifecycle of a UISceneSession
.
The default implementation of scene(_:willConnectTo:options:)
creates your initial content view (ContentView
if you’re using SwiftUI), creates a new UIWindow
, sets the window’s rootViewController
and makes this window the key window. You might think of this window as the window that your user sees. This, unfortunately, is not the case. Windows have been around since before iOS 13 and they represent the viewport that your app operates in. So, the UISceneSession
controls the visible window that the user sees, the UIWindow
you create is the container view for your application.
In addition to setting up initial views, you can use scene(_:willConnectTo:options:)
to restore your scene UI in case your scene has disconnected in the past. For example, because it was sent to the background. You can also read the connectionOptions
object to see if your scene was created due to a HandOff request or maybe to open a URL. I will show you how to do this later in this blog post.
Once your scene has connected, the next method in your scene’s lifecycle is sceneWillEnterForeground(_:)
. This method is called when your scene will take the stage. This could be when your app transitions from the background to the foreground, or if it’s just becoming active for the first time. Next, sceneDidBecomeActive(_:)
is called. This is the point where your scene is set up, visible and ready to be used.
When your app goes to the background, sceneWillResignActive(_:)
and sceneDidEnterBackground(_:)
are called. I will not go into these methods right now since their purpose varies for every application, and the comments in the Xcode template do a pretty good job of explaining when these methods are called. Actually, I’m sure you can figure out the timing of when these methods are called yourself.
A more interesting method is sceneDidDisconnect(_:)
. Whenever your scene is sent to the background, iOS might decide to disconnect and clear out your scene to free up resources. This does not mean your app was killed or isn’t running anymore, it simply means that the scene passed to this method is not active anymore and will disconnect from its session.
Note that the session itself is not necessarily discarded too, iOS might decide to reconnect a scene to a scene session at any time, for instance when a user brings a particular scene to the foreground again.
The most important thing to do in sceneDidDisconnect(_:)
is to discard any resources that you don’t need to keep around. This could be data that is easily loaded from disk or the network or other data that you can recreate easily. It’s also important to make sure you retain any data that can’t be easily recreated, like for instance any input the user provided in a scene that they would expect to still be there when they return to a scene.
Consider a text processing app that supports multiple scenes. If a user is working in one scene, then backgrounds it to do some research on Safari and change their music in Spotify, they would absolutely expect all their work to still exist in the text processing app, even though iOS might have disconnected the text processing app’s scene for a while. To achieve this, the app must retain the required data, and it should encode the current app state in an NSUserActivity
object that can be read later in scene(_:willConnectTo:options:)
when the scene is reconnected.
Since this workflow of connecting, disconnecting and reconnecting scenes is going to separate the good apps from the great, let’s have a look at how you can implement state restoration in your app.
Performing additional scene setup
There are several reasons for you to have to perform additional setup when a scene gets set up. You might have to open a URL, handle a Handoff request or restore state. In this section, I will focus mostly on state restoration since that’s possibly the most complex scenario you might have to handle.
State restoration starts when your scene gets disconnected and sceneDidDisconnect(_:)
is called. At this point, it's important that your application already has a state set up that can be restored later. The best way to do this is to use NSUserActivity
in your application. If you’re using NSUserActivity
to support Handoff, Siri Shortcuts, Spotlight indexing and more, you don’t have a lot of extra work to do. If you don’t use NSUserActivity
yet, don’t worry. A simple user activity might look a bit as follows:
let activity = NSUserActivity(activityType: "com.donnywals.DocumentEdit")
activity.userInfo = ["documentId": document.id]
Note that this user activity is not structured how Apple recommends it, it’s a very bare example intended to illustrate state restoration. For a complete guide on NSUserActivity
, I recommend that you take a look at Apple’s documentation on this topic.
When the time comes for you to provide a user activity that can be restored at a later time, the system calls stateRestorationActivity(for:)
method on your SceneDelegate
. Note that this method is not part of the default template
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
return scene.userActivity
}
Doing this associates the currently active user activity for a scene with the scene session. Remember that whenever a scene is disconnected, the UISceneSession
that owns the UIScene
is not discarded to allow the session to reconnect to a scene. When this happens, scene(_:willConnectTo:options:)
is called again. In this method, you have access to the UISceneSession
that owns the UIScene
so you can read the session’s stateRestorationActivity
and restore the application state as needed:
if let activity = session.stateRestorationActivity,
activity.activityType == "com.donnywals.DocumentEdit",
let documentId = activity.userInfo["documentId"] as? String {
// find document by ID
// create document viewcontroller and present it
}
Of course, the fine details of this code will vary based on your application, but the general idea should be clear.
If your UISceneSession
is expected to handle a URL, you can inspect the connectionOptions
object’s urlContexts
to find URLs that your scene should open and information about how your application should do this:
for urlContext in connectionOptions.urlContexts {
let url = urlContext.url
let options = urlContext.options
// handle url and options as needed
}
The options
object will contain information about whether your scene should open the URL in place, what application requested this URL to be opened and other metadata about the request.
The basics of state restoration in iOS 13 with the SceneDelegate
are surprisingly straightforward, especially since it's built upon NSUserActivity
which means that a lot of applications won’t have to do too much work to begin supporting state restoration for their scenes.
Keep in mind that if you want to have support for multiple scenes for your app on iPadOS, scene restoration is especially important since iOS might disconnect and reconnect your scenes when they switch from the foreground to the background and back again. Especially if your application allows a user to create or manipulate objects in a scene, a user would not expect their work to be gone if they move a scene to the background for a moment.
In summary
In this blog post, you have learned a lot. You learned what roles the AppDelegate
and SceneDelegate
fulfill in iOS 13 and what their lifecycles look like. You now know that the AppDelegate
is responsible for reacting to application-level events, like app launch for example. The SceneDelegate
is responsible for scene lifecycle related events. For example, scene creation, destruction and state restoration of a UISceneSession
. In other words, the main reason for Apple to add UISceneDelegate
to iOS 13 was to create a good entry point for multi-windowed applications.
After learning about the basics of UISceneDelegate
, you saw a very simple example of what state restoration looks like in iOS 13 with UISceneSession
and UIScene
. Of course, there is much more to learn about how your app behaves when a user spawns multiple UISceneSession
s for your app, and how these scenes might have to remain in sync or share data.
If you want to learn more about supporting multiple windows for your iPad app (or your macOS app), make sure to check out my post Adding support for multiple windows to your iPadOS app. Thanks for reading, and don’t hesitate to reach out on Twitter if you have any questions or feedback for me.