iOS 13 toe-dipping: Multi-window support on iPad
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
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.
Supporting window-based drop targets
A nice interaction with multi-window support is dragging an object in an app, and dragging it to the side of the screen to create a new window for that object. Getting this to work was a bit tricky in the first iOS 13 betas, but this has since been resolved. There are different ways to do this:
One note first: 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 don’t implement it or make sure it returns false
for cases where you do want to enable the window-based drop targets.
In the following code-samples, myObject
is an object that you might already be using for drag items to support regular Drag & Drop.
1. User activity
NSUserActivity
is playing an more and more important role, so this is probably the recommended approach. It is also the one taken in Apple’s sample app “Gallery”. There are two ways to achieve this:
let provider = NSItemProvider(object: myObject)
let activity = NSUserActivity(activityType: myActivityType)
provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)]
Make sure the visibility there is .all
, as .team
or .ownProcess
will not work.
Alternatively, you can also pass the activity directly to the NSItemProvider
:
let activity = NSUserActivity(activityType: myActivityType)
let provider = NSItemProvider(object: activity)
provider.registerObject(myObject, visibility: .all)
return [UIDragItem(itemProvider: provider)]
2. URL schemes
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://..."!))!
provider.registerObject(myObject, visibility: .all)
return [UIDragItem(itemProvider: provider)]
3. Universal links
I haven’t tested this myself, but universal links should work out of the box, similar to the URL scheme method above.
4. File URLs(?)
While this is mentioned in the 212 session as being support, I couldn’t yet get this working (up to iOS 13 beta 3). I tried using the fileURL
from an UIDocument
, but I am not getting the drop targets.
Further 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 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 atarget-content-id
APNS key. - For app shortcuts,
UIApplicationShortcutItem
has a newtargetContentIdentifier
field. - For user activities,
NSUserActivity
also has a newtargetContentIdentifier
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
- Session 212 “Introducing Multiple Windows on iPad” is the essential one, don’t miss it. Also check out the sample code.
- Session 258 “Architecting Your App for Multiple Windows” is a good follow-up, digging more into the new
UISceneDelegate
, the lifecycles, a bit on state restoration and how to keep your scenes in sync2. - Session 246 “Window Management in Your Multitasking App” has good sample code for requesting, updating and deleting sessions, as well as keeping your snapshots up-to-date and re-iterates which animations to use.
- Session 242 “Targeting Content with Multiple Windows” covers in details how to define the activation conditions that iOS needs to open the appropriate scene when the user launches your app through notifications, shortcut items, or user activites.
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 in beta 1+2, with thanks to @lostformars. 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.
- 3 July: Updated for iOS 13 beta 3, where window-based Drag & Drop works with NSUserActivity, too.
-
All the people who have warned that these are anti-patterns better to be avoided can have a smug smile of told-you-so. ↩
-
Now is a great time to get familiar with reactive programming. ↩