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.

5 (10) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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:

  1. Import SwiftUI, since you need to use a hosting view and invoke a SwiftUI view here.
  2. Declare conformance to UIWindowSceneDelegate, allowing you to respond to window scene events.
  3. Create a variable to hold a UIWindow. You populate this when you create your scene.
  4. Implement scene(_:willConnectTo:options:), which allows you to define the startup environment and views.
  5. Create a new window using the UIWindowScene passed to you by UIKit.
  6. Instantiate a new instance of AddNoteView and set it as the root view of a new UIHostingController. Since it’s not running in a modal context, pass false for addNotePresented argument.
  7. Set the window property to the new window.
  8. 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:

  1. Open Info.plist again.
  2. Expand Application Scene Manifest.
  3. Open Scene Configuration ▸ Application Session Role nodes.
  4. Tap the plus (+) button next to Application Session Role to add a new entry.
  5. Drag this entry underneath the Default Configuration entry.
  6. 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.
  7. 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.
  8. 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:

Application Scene Manifest

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:

  1. Only runs on an iPad. iPhone devices do not currently support multiple scenes.
  2. Creates a new button with the title New Window and adds some styling to it.
  3. Instantiates a user activity with the enum you created earlier in this section.
  4. Requests a new scene session from UIApplication and passes it the user activity you created in the previous section.
  5. Sets the binding variable addNotePresented to false, 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:

New Window

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:

  1. Create a variable to hold the current user activity.
  2. Check the connection options for user activities and attempt to generate an ActivityIdentifier.
  3. Use the activity if found. If not, default to list.
  4. Create a new scene configuration by using the convenience method on ActivityIdentifier.
  5. 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.

Split View