The code samples from Apple’s WWDC sessions and the sample code for supporting multiple windows, are a bit inconsistent which can make it a bit bumpy to get started. Here is the setup for supporting multiple windows on iPad that worked will in my experimentations so far:

1) Have an AppDelegate as usual, and keep it marked as @ApplicationMain.

2) Create a new SceneDelegate class that implements UIWindowSceneDelegate.

3) Tick the box, then add Info.plist configuration as follows:

Application Scene Manifest
Application Scene Manifest

4) As the SceneDelegate, if you’re using storyboards the following is sufficient:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?
}

Contrary to some of the session videos, do not instantiate that window manually in scene(_:willConnectTo:options:) if you’re using storyboards and specified the storyboard in the scene manifest above. Only if that’s not the case, do instantiate it manually and then also set the root view controller.

And with that you’re good to go.

Your next step is likely that you’ll want to do is switch your state restoring to use NSUserActivity rather than manually restoring the view hierarchy. You’ll want to migrate UI life-cycle code over from your app delegate to the scene delegate.

And then you want to test the 💩 out of your app, to make sure that it works with many of your view controllers being running in parallel and your individual scenes disconnecting and re-connecting. Be wary of global state and singletons1 such as the standard user default; UISceneSession has a useful userInfo property for this.

Stumbling blocks

Q: Why am I only getting a blank black screen?

The sample code in the WWDC slides isn’t all that consistent about this. Check that you follow the steps from above.


Q: I want to configure my window with the user activity from the session, but at the time the session videos showed there’s no root view controller set. Help!

I had this issue after following the code from session 258. Make sure to not overwrite your scene delegate’s var window if its already set. It will already be set properly with the root view controller, if you’re using Storyboards and specified your application scene manifest in the Info.plist.


Q: How do I get the nice window-based drop targets for drag items started in my application?

Great question. The answer from the 212 session is to 1) use Universal Links, 2) use file URLs for documents that your app is claiming as the owner, or 3) attach a NSUserActivity to your existing UIDragItem instances. I tried 2 and 3 and neither worked.

My attempt to attach a NSUserActivity follows Apple’s sample app and looks like this:

let provider = NSItemProvider(object: myObject)
    
let activity = NSUserActivity(activityType: myActivityType)
provider.registerObject(activity, visibility: .ownProcess)

return [UIDragItem(itemProvider: provider)]

This doesn’t work in my testing, so it might not be supported in iOS 13 Beta 1 yet.

The good news is that I got this working with the kind help from @lostformars. First make sure you have a URL scheme defined, then pass that to the init(contentsOf:) constructor of NSItemProvider. This is rather unexpected as the documentation says that the URL you pass to that method should point to an existing file. Alas, it works:

let provider = NSItemProvider(contentsOf: URL(string: "myScheme://..."!))!

// Optionally register previous object to
// also keep supporting regular Drag & Drop
provider.registerObject(myObject, visibility: .all)

return [UIDragItem(itemProvider: provider)]

If you did that and it’s still not working, UIKit won’t create any new windows when dragging out of a UITableView or UICollectionView if you implemented dragSessionIsRestrictedToDraggingApplication delegate method and it returns true, so make sure it returns false instead.


Q: How do I determine which scene to activate when the app is opened through a notification, app shortcut or user activities?

This is the topic of session 242. You don’t determine this directly yourself, but rather iOS determines it. It does so by looking at the target content identifier that you have attached with the appropriate object. If you support Universal Links use those as those identifiers, otherwise use a URL scheme that identifiers the content in your app. Then attach it as follows:

  • For notifications, add it to your UNNotificationContent as a target-content-id APNS key.
  • For app shortcuts, UIApplicationShortcutItem has a new targetContentIdentifier field.
  • For user activities, NSUserActivity also has a new targetContentIdentifier field.

Then add those to your scene’s activation conditions. For document-based apps where a scene can show multiple tabs or documents it could look like this:

let conditions = scene.activationConditions

// This scene can display any content, ...
conditions.canActivateForTargetContentIdentifierPredicate = 
  NSPredicate(value: true)

// ..., but we prefer the current open tabs
let subpredicates = openTabs.map { 
  NSPredicate(format: "self == %@", $0.targetContentIdentifier) 
}
conditions.prefersToActivateForTargetContentIdentifierPredicate =
  NSCompoundPredicate(orPredicateWithSubpredicates: subpredicates)

Session videos

Have fun!


Changelog:

  • 14 June: Fixed first paragraph, linking to sample code and videos. Adding note the drag to open new window also isn’t working in sample code.
  • 16 June: Added working answer for how to get the window-based Drag & Drop targets. Added information on determining what scene to activate.
  • 18 June: Added another stumbling block that can prevent window-based Drag & Drop targets from working. Thanks to @simonbs for pointing this out.
  1. All the people who have warned that these are anti-patterns better to be avoided can have a smug smile of told-you-so. 

  2. Now is a great time to get familiar with reactive programming