{"id":17335983,"url":"https://github.com/basthomas/board","last_synced_at":"2025-10-14T17:50:23.404Z","repository":{"id":76785669,"uuid":"210286716","full_name":"BasThomas/Board","owner":"BasThomas","description":"iPadOS workshop at FrenchKit","archived":false,"fork":false,"pushed_at":"2019-10-12T15:14:22.000Z","size":49,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-27T07:26:01.374Z","etag":null,"topics":["code-along","drag-and-drop","ios","ios13","ipad","ipados","multi-windows","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/BasThomas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-09-23T07:04:32.000Z","updated_at":"2020-01-13T18:17:38.000Z","dependencies_parsed_at":null,"dependency_job_id":"b99850c1-c74f-4231-8566-28aa58fe0a2c","html_url":"https://github.com/BasThomas/Board","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/BasThomas/Board","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BasThomas%2FBoard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BasThomas%2FBoard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BasThomas%2FBoard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BasThomas%2FBoard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/BasThomas","download_url":"https://codeload.github.com/BasThomas/Board/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BasThomas%2FBoard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279020054,"owners_count":26086807,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-14T02:00:06.444Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-along","drag-and-drop","ios","ios13","ipad","ipados","multi-windows","swift"],"created_at":"2024-10-15T15:27:03.538Z","updated_at":"2025-10-14T17:50:23.376Z","avatar_url":"https://github.com/BasThomas.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Board\n\n![board](https://user-images.githubusercontent.com/4190298/66703486-708d9800-ed13-11e9-8907-8e87c8179339.png)\n\nA gave a classroom/workshop with this project at [FrenchKit](https://frenchkit.fr)\n2019. Before starting it, I gave [this introduction](https://speakerdeck.com/basthomas/an-introduction-to-ipados-workshop-5b614f1f-aef8-4aaf-b724-fd7d6695acf1)\nthat introduces iPadOS multi-window support. This is also the topic of this\nworkshop.\n\n### ... and now what?\n\nAlthough it may be hard to follow this without doing an in-person workshop,\nbelow you will find the steps we went through, including some words of advice\nand my thoughts, so you can take on this project yourself. If you do, let me\nknow how it goes!\n\nOf course, you're supposed to start with the [`Starter`](/Starter/) project\nand go from there!\n\n## Adding support for multiple windows\n\n### `SceneDelegate`\n\nWe'll need to tell our application that we want to support multiple windows.\nTo do so, go to the `Info.plist` and add the required configuration.\n\n\u003cdetails\u003e\n\u003csummary\u003eStep 1\u003c/summary\u003e\n\n```plist\n\u003ckey\u003eUIApplicationSceneManifest\u003c/key\u003e\n\u003cdict\u003e\n    \u003ckey\u003eUIApplicationSupportsMultipleScenes\u003c/key\u003e\n    \u003ctrue/\u003e\n    \u003ckey\u003eUISceneConfigurations\u003c/key\u003e\n    \u003cdict\u003e\n        \u003ckey\u003eUIWindowSceneSessionRoleApplication\u003c/key\u003e\n        \u003carray\u003e\n            \u003cdict\u003e\n                \u003ckey\u003eUISceneConfigurationName\u003c/key\u003e\n                \u003cstring\u003eDefault Configuration\u003c/string\u003e\n                \u003ckey\u003eUISceneDelegateClassName\u003c/key\u003e\n                \u003cstring\u003e$(PRODUCT_MODULE_NAME).SceneDelegate\u003c/string\u003e\n                \u003ckey\u003eUISceneStoryboardFile\u003c/key\u003e\n                \u003cstring\u003eMain\u003c/string\u003e\n            \u003c/dict\u003e\n        \u003c/array\u003e\n    \u003c/dict\u003e\n\u003c/dict\u003e\n```\n\u003c/details\u003e\n\nNow that we have the initial setup for the `Info.plist`, we need to create our\n[`SceneDelegate`](https://developer.apple.com/documentation/uikit/uiscenedelegate) class.\n\n\u003cdetails\u003e\n\u003csummary\u003eStep 2\u003c/summary\u003e\n\n```swift\n// SceneDelegate.swift\nimport UIKit\n\nclass SceneDelegate: UIResponder, UIWindowSceneDelegate {\n    var window: UIWindow?\n}\n```\n\u003c/details\u003e\n\nNow that we have that setup, we'll need to add a non-default scene\nconfiguration. This will be showing our card, rather than our \"default\" app\nscene.\n\n\u003cdetails\u003e\n\u003csummary\u003eStep 3\u003c/summary\u003e\n\nAdd the following within the `UIWindowSceneSessionRoleApplication` array:\n\n```plist\n\u003cdict\u003e\n    \u003ckey\u003eUISceneConfigurationName\u003c/key\u003e\n    \u003cstring\u003eCard Configuration\u003c/string\u003e\n    \u003ckey\u003eUISceneDelegateClassName\u003c/key\u003e\n    \u003cstring\u003e$(PRODUCT_MODULE_NAME).CardSceneDelegate\u003c/string\u003e\n    \u003ckey\u003eUISceneStoryboardFile\u003c/key\u003e\n    \u003cstring\u003eCard\u003c/string\u003e\n\u003c/dict\u003e\n```\n\u003c/details\u003e\n\n### `NSUserActivity`\n\nGreat! Now, we'll use [`NSUserActivity`](https://developer.apple.com/documentation/foundation/nsuseractivity)\nto be able to create our newly created configuration.\n\n\u003cdetails\u003e\n\u003csummary\u003eStep 4\u003c/summary\u003e\n\n```swift\n// in Card.swift\nstatic let userActivityType = \"fr.frenchkit.card\"\nstatic let userActivityTitle = \"showCardDetail\"\nvar userActivity: NSUserActivity {\n    let userActivity = NSUserActivity(activityType: Card.userActivityType)\n    userActivity.title = Card.userActivityTitle\n    userActivity.userInfo = [\n        \"content\": content\n    ]\n    return userActivity\n}\n```\n\u003c/details\u003e\n\n... and set up all the magic in a new `SceneDelegate`; namely our just created\n`CardSceneDelegate`.\n\n\u003cdetails\u003e\n\u003csummary\u003eStep 5\u003c/summary\u003e\n\n```swift\n// in CardSceneDelegate.swift\nimport UIKit\n\nclass CardSceneDelegate: UIResponder, UIWindowSceneDelegate {\n    var window: UIWindow?\n\n    func stateRestorationActivity(for scene: UIScene) -\u003e NSUserActivity? {\n        return scene.userActivity\n    }\n\n    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {\n        guard let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity else { return }\n        if !configure(window: window, with: userActivity) {\n            print(\"Failed to restore from \\(userActivity)\")\n        }\n    }\n\n    func configure(window: UIWindow?, with activity: NSUserActivity) -\u003e Bool {\n        guard activity.title == Card.userActivityTitle else { return false }\n        guard\n            let content = activity.userInfo?[\"content\"] as? String else { fatalError(\"Could not get valid user info from activity\") }\n\n        let controller = UIStoryboard(name: \"Card\", bundle: .main)\n            .instantiateViewController(identifier: CardViewController.identifier) as! CardViewController\n        controller.card = Card(content: content)\n\n        window?.rootViewController = controller\n        return true\n    }\n}\n```\n\u003c/details\u003e\n\nTo make sure the app knows which user activities to listen to, we'll need to\nmake one more edit to the `Info.plist`.\n\n\u003cdetails\u003e\n\u003csummary\u003eStep 6\u003c/summary\u003e\n\n```plist\n\u003ckey\u003eNSUserActivityTypes\u003c/key\u003e\n\u003carray\u003e\n    \u003cstring\u003efr.frenchkit.card\u003c/string\u003e\n\u003c/array\u003e\n```\n\u003c/details\u003e\n\n### Drag and Drop\n\nAlmost there, almost there. We'll add [drag and drop](https://developer.apple.com/ios/drag-and-drop/)\nsupport, which works very nicely with the configurations we created, allowing\nfor an intuitive way to create the new session.\n\n\u003cdetails\u003e\n\u003csummary\u003eStep 7\u003c/summary\u003e\n\n```swift\n// in BoardCollectionViewController\noverride func viewDidLoad() {\n    super.viewDidLoad()\n    collectionView.dragDelegate = self\n}\n\nextension BoardCollectionViewController: UICollectionViewDragDelegate {\n    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -\u003e [UIDragItem] {\n        let selectedCard = columns[indexPath.section].cards[indexPath.row]\n\n        let userActivity = selectedCard.userActivity\n        let itemProvider = NSItemProvider(object: userActivity)\n        \n        let dragItem = UIDragItem(itemProvider: itemProvider)\n        dragItem.localObject = selectedCard\n\n        return [dragItem]\n    }\n}\n```\n\u003c/details\u003e\n\n### `AppDelegate`\n\nAnd for the grand finale, we'll make sure our application handles which\nconfiguration to connect to, and when.\n\n\u003cdetails\u003e\n\u003csummary\u003eStep 8\u003c/summary\u003e\n\n```swift\n// in AppDelegate.swift\nfunc application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -\u003e UISceneConfiguration {\n    let configurationName: String\n    if options.userActivities.first?.activityType == Card.userActivityType {\n        configurationName = \"Card Configuration\"\n    } else {\n        configurationName = \"Default Configuration\"\n    }\n    return .init(name: configurationName, sessionRole: connectingSceneSession.role)\n}\n```\n\u003c/details\u003e\n\nBuild and run. You can now drag a card and drop it at the screen edge to create\na new scene. 🎉\n\n### Scene Destruction\n\nOne more thing... the new scene has a close button, but it doesn't do anything.\nLet's hook that up.\n\n\u003cdetails\u003e\n\u003csummary\u003eStep 9\u003c/summary\u003e\n\n```swift\n// in CardViewController.swift\n@IBAction func close(_ sender: Any) {\n    guard let session = view.window?.windowScene?.session else { fatalError(\"No session found for this view controller\") }\n    let options = UIWindowSceneDestructionRequestOptions()\n    options.windowDismissalAnimation = .default\n    application.requestSceneSessionDestruction(session, options: options)\n}\n```\n\u003c/details\u003e\n\n## Where to go from here?\n\nGo wild! There's lots more to look into. Data syncing, supporting drag and drop\nfor the \"Add Column\" screen (and a configuration!), refreshing outdated\nsessions, preventing duplicate sessions from being created... the list goes on.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbasthomas%2Fboard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbasthomas%2Fboard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbasthomas%2Fboard/lists"}