Adopting Scenes in iPadOS
In this Adopting Scenes tutorial, you’ll learn how to support multiple windows by creating a new iPadOS app. You’ll also learn how to update existing iOS 12 apps to support multiple windows. By Mark Struzinski.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Adopting Scenes in iPadOS
30 mins
- Getting Started
- Scene Components
- UIWindowScene
- UIWindowSceneDelegate
- UISceneConfiguration
- UISceneSession
- iPadOS and Multiple Scenes
- Creating New Windows
- Through the App Exposé Multitasking View
- Via an Explicit User Interaction
- Via Drag and Drop
- The Example App
- The Plan
- Adding Multiple Scene Support
- Update the Info.plist
- Adding a New Scene
- Create a New Scene Delegate
- Add a New Scene to the Scene Manifest
- Add UI to Display the New Scene
- Create a Constant Declaration for Scene Names
- Create an enum Declaration for User Activities
- Add UI to Create a New Window
- Update the App Delegate to Configure the New Scene
- Keeping Scenes up to Date
- Keeping Foreground Scenes up to Date
- Keeping Background Scenes up to Date
- Attaching Identifying Information to Scenes
- Updating Scenes from the Background
- Updating Your App From iOS 12
- Update Info.plist
- Add a Scene Delegate
- Update the Scene Delegate
- Where to Go From Here?
Adding a New Scene
In the current state, when you tap the New Note button, a modal sheet opens to enter a new note. After you dismiss the window, the new note shows up in the list.
What if a user wants to enter new notes in one window and watch the list refresh in a different one side-by-side? This is a valid use case and increases the usefulness of the app. But how, you ask, do you add support for a brand new scene?
Create a New Scene Delegate
First, you need to add a new scene delegate to respond to events for the new scene. Under the App group in Xcode, add a new Swift file by selecting File ▸ New ▸ File and picking the Swift File template. Name the new file CreateDelegate.swift and click Create.
Add the following code to the new file:
// 1
import SwiftUI
// 2
class CreateDelegate: UIResponder, UIWindowSceneDelegate {
// 3
var window: UIWindow?
// 4
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
if let windowScene = scene as? UIWindowScene {
// 5
let window = UIWindow(windowScene: windowScene)
// 6
window.rootViewController = UIHostingController(
rootView: AddNoteView(addNotePresented: .constant(false))
)
// 7
self.window = window
// 8
window.makeKeyAndVisible()
}
}
}
With this code, you:
- Import SwiftUI, since you need to use a hosting view and invoke a SwiftUI view here.
- Declare conformance to
UIWindowSceneDelegate
, allowing you to respond to window scene events. - Create a variable to hold a
UIWindow
. You populate this when you create your scene. - Implement
scene(_:willConnectTo:options:)
, which allows you to define the startup environment and views. - Create a new window using the
UIWindowScene
passed to you by UIKit. - Instantiate a new instance of
AddNoteView
and set it as the root view of a newUIHostingController
. Since it’s not running in a modal context, passfalse
foraddNotePresented
argument. - Set the
window
property to the new window. - Make the new window visible.
Add a New Scene to the Scene Manifest
Next, you need to declare support for the new scene in the scene manifest. This is where you’ll declare your new scene and tell UIKit where to get the scene delegate for it. To do so:
- Open Info.plist again.
- Expand Application Scene Manifest.
- Open Scene Configuration ▸ Application Session Role nodes.
- Tap the plus (+) button next to Application Session Role to add a new entry.
- Drag this entry underneath the Default Configuration entry.
- Expand the new entry and delete the Class Name and Storyboard Name entries. You’ll use the default for the class, which is
UIWindowScene
. There is no need to customize this. Since you’re invoking a SwiftUI view, there is no need to declare a storyboard name. - Enter the value $(PRODUCT_MODULE_NAME).CreateDelegate for Delegate Class Name. This tells UIKit to use your new
CreateDelegate
when initializing the new scene from the main target module. - Enter the value Create Configuration for Configuration Name. This is the name UIKit will use to look up configuration setup to create your new scene.
Your Application Scene Manifest entry should now look like this:
Add UI to Display the New Scene
You’ve declared support for the new scene, but now you need a way to display it.
To do so, add a button to the New Note view that will allow the user to make the modal a new window. This will create a new scene and place it side by side with the existing note list scene.
UIKit always calls application(_:configurationForConnecting:options:)
in your UIApplicationDelegate
class to determine which scene configuration to use when bootstrapping a new scene.
This is where you customize the startup experience. Hooking into the NSUserActivity
system is how you specify scene creation options.
When UIKit creates the new scene, it will pass any existing NSUserActivity
objects into the UIScene.connectionOptions
parameter of this method. You can use that activity to inform UIKit which scene configuration to use.
First, select NoteList group in the Project navigator. Create a new group using File ▸ New ▸ Group and name it Constants. Select File ▸ New ▸ File and pick the Swift File template. Name the new file SceneConfigurationNames.swift, make sure Constants group is selected as the destination, and click Create.
Add the following code underneath import Foundation
:
struct SceneConfigurationNames {
static let standard = "Default Configuration"
static let create = "Create Configuration"
}
This creates some constants you can use to reference configuration names.
To create an enum to reference user activities, select File ▸ New ▸ File and pick the Swift File template. Name the file ActivityIdentifier.swift, select the Constants group as the destination, and click Create.
Enter the following code:
import UIKit
enum ActivityIdentifier: String {
case list = "com.raywenderlich.notelist.list"
case create = "com.raywenderlich.notelist.create"
func sceneConfiguration() -> UISceneConfiguration {
switch self {
case .create:
return UISceneConfiguration(
name: SceneConfigurationNames.create,
sessionRole: .windowApplication
)
case .list:
return UISceneConfiguration(
name: SceneConfigurationNames.standard,
sessionRole: .windowApplication
)
}
}
}
This specifies an easy-to-use and type-safe enum that you can use to identify scenes via NSUserActivity
. This prevents the need to pass string literals around when referencing NSUserActivity identifiers. It also creates a simple convenience method to generate a scene configuration from a given activity identifier.
Now you can connect all of this together by adding a New Window button to the Add Note view.
Open AddNoteFormButtonView.swift. Inside the HStack
, right under the first button declaration, add the following code:
// 1
if UIDevice.current.userInterfaceIdiom == .pad {
// 2
Button("New Window") {
// 3
let userActivity = NSUserActivity(
activityType: ActivityIdentifier.create.rawValue
)
// 4
UIApplication
.shared
.requestSceneSessionActivation(
nil,
userActivity: userActivity,
options: nil,
errorHandler: nil)
// 5
self.addNotePresented = false
}.padding(EdgeInsets(top: 12, leading: 20, bottom: 12, trailing: 20))
.foregroundColor(Color.white)
.background(Color(red: 46/255, green: 204/255, blue: 113/255))
.cornerRadius(10)
}
Here is what this code does:
- Only runs on an iPad. iPhone devices do not currently support multiple scenes.
- Creates a new button with the title New Window and adds some styling to it.
- Instantiates a user activity with the enum you created earlier in this section.
- Requests a new scene session from
UIApplication
and passes it the user activity you created in the previous section. - Sets the binding variable
addNotePresented
tofalse
, which will tell the note list view to dismiss the modal.
Build and run. Tap the plus button on the top-right to check out the new button on the bottom of the Add Note view:
Finally, you need to update the app delegate to use the new configuration for the create user activity if it’s in the connection options.
Open AppDelegate.swift, find and replace application(_:configurationForConnecting:options:)
with this:
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions)
-> UISceneConfiguration {
// 1
var currentActivity: ActivityIdentifier?
// 2
options.userActivities.forEach { activity in
currentActivity = ActivityIdentifier(rawValue: activity.activityType)
}
// 3
let activity = currentActivity ?? ActivityIdentifier.list
// 4
let sceneConfig = activity.sceneConfiguration()
// 5
return sceneConfig
}
Here, you:
- Create a variable to hold the current user activity.
- Check the connection options for user activities and attempt to generate an
ActivityIdentifier
. - Use the activity if found. If not, default to list.
- Create a new scene configuration by using the convenience method on
ActivityIdentifier
. - Return the new scene configuration.
Build and run one more time.
Attempt to add a new note, then tap the New Window button. This time, a new window will open in split view with the note list. Excellent! You added support for a new scene.