Add iPad OS multiple windows support

iPad OS 13 introduces the support of multi windows. You can now have multiple “instances” or copies of the same app through separated windows.

The implementation of such a feature requires some re-work of the current app life cycle.

The first step is to add a SceneDelegate to your app. If you don't know how to, I suggest my previous post about it.

Now, understand that a window, in iOS, is a scene and as Apple's documentation says :

A scene contains the windows and view controllers for presenting one instance of your UI. Each scene also has a corresponding UIWindowSceneDelegate object, which you use to coordinate interactions between UIKit and your app.

So how it's going to work will be pretty basic :

  • The user will drag and drop an item to create a new window of your app.
  • The system will instantiate a new Scene, requesting a scene to connect to your app's session.
  • The app's SceneDelegate will be notified that a scene has been requested to connect to it's session.
    It will evaluate what kind of scene is requested by it's connection options, and will present the correct view controller.

Therefore, all of your presentation logic will gravitate around your SceneDelegate's sceneWillConnectToSession function.

You will get the connection options object (of type ConnectionOptions) in parameter of the sceneWillConnectToSession function.
It's prototype looks like this :

    open class ConnectionOptions : NSObject {
        open var urlContexts: Set<UIOpenURLContext> { get }
        open var sourceApplication: String? { get }
        open var handoffUserActivityType: String? { get }
        open var userActivities: Set<NSUserActivity> { get }
        open var notificationResponse: UNNotificationResponse? { get }
        open var shortcutItem: UIApplicationShortcutItem? { get }
        open var cloudKitShareMetadata: CKShareMetadata? { get }
    }

The property that we will work with is the userActivities.
This is how we will differentiate our Scenes.
So we need to set our different activites types.

In your project's info.plist, add a new array called NSUserActivityTypes in which you will create every type of window you will want to be presentable with a string identifier.

To illustrate this article I will make a restaurants list app.
In this app, there is only one window type I will need : the one for a restaurant detail page, displaying the info, address, and reviews of a restaurant.

So I will create one type "restaurant". This is what I have.

    <key>NSUserActivityTypes</key>
    <array>
        <string>restaurant</string>
    </array>

Now to make it easier to work with activity types in the code I will create an enum (of strings) with every activity type specified.

enum ActivityType: String {
    case restaurant
}

Now all I have to do in my SceneDelegate, is go in the SceneWillConnectToSession method and distinguish the requested scene and present the right ViewController.

Here is how to do this :

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }
        if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
            // User request new window with userActivity
            switch MyActivityType(rawValue: userActivity.activityType) {
            case .restaurant:
                // Present the restaurant view controller
            case .none:
                // Present the hone view controller
            }
        } else {
            // This is an initial app launch
            self.present(viewController: ViewController(), scene: scene)
            // You don't need an else case if you have a main storyboard
        }
    }

To present a ViewController from here, if you are using a main storyboard, the window property will automatically be loaded with the storyboard's initial view controller.
So you just need to check if the rootViewController is present (and is a NavigationController) and then push your ViewController on it, like this :

if let navigationController = window?.rootViewController as? UINavigationController {
    navigationController.pushViewController(yourViewController, animated: false)
}

If you are not using a main storyboard, you need initialize the window and set the root, like this :

if let windowScene = scene as? UIWindowScene {
    self.window = UIWindow(windowScene: windowScene)
    let mainNavigationController = UINavigationController(rootViewController: yourViewController)
    self.window!.rootViewController = mainNavigationController
    self.window!.makeKeyAndVisible()
}

🥳 Now your app can open windows ! 🎉

Requesting new windows from the code

Having a "Open in a new window" button right in your view could be interesting in some cases.
To request a new window, you just need to request a scene session, with the activity you want.

If you want a new "restaurant" window :

let activity: NSUserActivity = NSUserActivity(activityType: ActivityType.restaurant.rawValue)
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil, errorHandler: nil)

You can use userInfo dictionary to pass more data such as an id.

And that's it : a new window will automatically be opened.

Enabling drag and drop from a Table/Collection view to open your detail view controller in a new window

Let’s say you have a TableView (or a CollectionView) of items which, on tap, will push some kind of detail ViewController, a pretty useful scenario.
In your CollectionView or TableView, you need to set a dragDelegate, and conform to the method itemForBeginningSessionAtIndexPath and return a DragItem containing the requested UserActivty.

Here is an example :

    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        // Create a userActivity with the activity type you want
        let userActivity = NSUserActivity(activityType: ActivityType.restaurant.rawValue)
        // You can use userInfo dictionary to pass more data such as an id.
        userActivity.userInfo!["id"] = self.data[indexPath.row].id()

        // Create an item provider with our activity
        let itemProvider = NSItemProvider()
        itemProvider.registerObject(userActivity, visibility: .all)

        // Create a drag item containing the itemProvider and return it in an array
        let dragItem = UIDragItem(itemProvider: itemProvider)
        return [dragItem]
    }

Now you can just drag any of you collection/table view cells to open windows.

Hope you enjoyed this multiple windows on iPad OS overview.
A sample app, with a restaurant list is available on my GitHub.

I will be glad to try to help if you have any question or issues.

Happy coding ! 😊

Leave a Comment