A gave a classroom/workshop with this project at FrenchKit 2019. Before starting it, I gave this introduction that introduces iPadOS multi-window support. This is also the topic of this workshop.
Although it may be hard to follow this without doing an in-person workshop, below you will find the steps we went through, including some words of advice and my thoughts, so you can take on this project yourself. If you do, let me know how it goes!
Of course, you're supposed to start with the Starter project
and go from there!
We'll need to tell our application that we want to support multiple windows.
To do so, go to the Info.plist and add the required configuration.
Step 1
<key>UIApplicationSceneManifest</key><dict> <key>UIApplicationSupportsMultipleScenes</key> <true/> <key>UISceneConfigurations</key> <dict> <key>UIWindowSceneSessionRoleApplication</key> <array> <dict> <key>UISceneConfigurationName</key> <string>Default Configuration</string> <key>UISceneDelegateClassName</key> <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string> <key>UISceneStoryboardFile</key> <string>Main</string> </dict> </array> </dict></dict>Now that we have the initial setup for the Info.plist, we need to create our
SceneDelegate class.
Step 2
// SceneDelegate.swift
import UIKit
classSceneDelegate:UIResponder,UIWindowSceneDelegate{varwindow:UIWindow?}Now that we have that setup, we'll need to add a non-default scene configuration. This will be showing our card, rather than our "default" app scene.
Step 3
Add the following within the UIWindowSceneSessionRoleApplication array:
<dict> <key>UISceneConfigurationName</key> <string>Card Configuration</string> <key>UISceneDelegateClassName</key> <string>$(PRODUCT_MODULE_NAME).CardSceneDelegate</string> <key>UISceneStoryboardFile</key> <string>Card</string></dict>Great! Now, we'll use NSUserActivity
to be able to create our newly created configuration.
Step 4
// in Card.swift
staticletuserActivityType="fr.frenchkit.card"staticletuserActivityTitle="showCardDetail"varuserActivity:NSUserActivity{letuserActivity=NSUserActivity(activityType:Card.userActivityType)
userActivity.title =Card.userActivityTitle
userActivity.userInfo =["content": content
]return userActivity
}... and set up all the magic in a new SceneDelegate; namely our just created
CardSceneDelegate.
Step 5
// in CardSceneDelegate.swift
import UIKit
classCardSceneDelegate:UIResponder,UIWindowSceneDelegate{varwindow:UIWindow?func stateRestorationActivity(for scene:UIScene)->NSUserActivity?{return scene.userActivity
}func scene(_ scene:UIScene, willConnectTo session:UISceneSession, options connectionOptions:UIScene.ConnectionOptions){guardlet userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity else{return}if !configure(window: window, with: userActivity){print("Failed to restore from \(userActivity)")}}func configure(window:UIWindow?, with activity:NSUserActivity)->Bool{guard activity.title ==Card.userActivityTitle else{returnfalse}guardlet content = activity.userInfo?["content"]as?Stringelse{fatalError("Could not get valid user info from activity")}letcontroller=UIStoryboard(name:"Card", bundle:.main).instantiateViewController(identifier:CardViewController.identifier)as!CardViewController
controller.card =Card(content: content)
window?.rootViewController = controller
returntrue}}To make sure the app knows which user activities to listen to, we'll need to
make one more edit to the Info.plist.
Step 6
<key>NSUserActivityTypes</key><array> <string>fr.frenchkit.card</string></array>Almost there, almost there. We'll add drag and drop support, which works very nicely with the configurations we created, allowing for an intuitive way to create the new session.
Step 7
// in BoardCollectionViewController
overridefunc viewDidLoad(){
super.viewDidLoad()
collectionView.dragDelegate =self}extensionBoardCollectionViewController:UICollectionViewDragDelegate{func collectionView(_ collectionView:UICollectionView, itemsForBeginning session:UIDragSession, at indexPath:IndexPath)->[UIDragItem]{letselectedCard=columns[indexPath.section].cards[indexPath.row]letuserActivity= selectedCard.userActivity
letitemProvider=NSItemProvider(object: userActivity)letdragItem=UIDragItem(itemProvider: itemProvider)
dragItem.localObject = selectedCard
return[dragItem]}}And for the grand finale, we'll make sure our application handles which configuration to connect to, and when.
Step 8
// in AppDelegate.swift
func application(_ application:UIApplication, configurationForConnecting connectingSceneSession:UISceneSession, options:UIScene.ConnectionOptions)->UISceneConfiguration{letconfigurationName:Stringif options.userActivities.first?.activityType ==Card.userActivityType {
configurationName ="Card Configuration"}else{
configurationName ="Default Configuration"}return.init(name: configurationName, sessionRole: connectingSceneSession.role)}Build and run. You can now drag a card and drop it at the screen edge to create a new scene. 🎉
One more thing... the new scene has a close button, but it doesn't do anything. Let's hook that up.
Step 9
// in CardViewController.swift
@IBActionfunc close(_ sender:Any){guardlet session = view.window?.windowScene?.session else{fatalError("No session found for this view controller")}letoptions=UIWindowSceneDestructionRequestOptions()
options.windowDismissalAnimation =.default
application.requestSceneSessionDestruction(session, options: options)}Go wild! There's lots more to look into. Data syncing, supporting drag and drop for the "Add Column" screen (and a configuration!), refreshing outdated sessions, preventing duplicate sessions from being created... the list goes on.
