Understanding the iOS 13 Scene Delegate

Published on: October 28, 2019

When 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:

Screenshot of the Info.plist file's scene manifest

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 UISceneSessions 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 using AppDelegate 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 UISceneSessions 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.

Categories

App Development

Subscribe to my newsletter