GitXplorerGitXplorer
B

Board

public
4 stars
0 forks
0 issues

Commits

List of commits on branch master.
Verified
c3205dbee5a26958b94f4269b51410a48a9cc3bb

Update README.md

BBasThomas committed 5 years ago
Verified
68ea151a601d64a9f022a4bb75c2ff54e15a333f

Update README.md

BBasThomas committed 5 years ago
Unverified
138d2c30a6ebb4cf7fb6e44a377d69d25f98bdff

Delete boilerplate comment

BBasThomas committed 5 years ago
Unverified
75d894d532f15ccc42f60bc6b3cca1afcefd51ac

Setup starter

BBasThomas committed 5 years ago
Unverified
fe0c852b87acca385febb9e96e86b9812371ad35

Support adding columns

BBasThomas committed 5 years ago
Unverified
5eef64b7e553916f0af92625d0a1d3d13fbb03ed

Support drag and drop

BBasThomas committed 5 years ago

README

The README file for this repository.

Board

board

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.

... and now what?

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!

Adding support for multiple windows

SceneDelegate

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

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: 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>

NSUserActivity

Great! Now, we'll use NSUserActivity to be able to create our newly created configuration.

Step 4
// in Card.swift
static let userActivityType = "fr.frenchkit.card"
static let userActivityTitle = "showCardDetail"
var userActivity: NSUserActivity {
    let userActivity = 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

class CardSceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        return scene.userActivity
    }

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let 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 { return false }
        guard
            let content = activity.userInfo?["content"] as? String else { fatalError("Could not get valid user info from activity") }

        let controller = UIStoryboard(name: "Card", bundle: .main)
            .instantiateViewController(identifier: CardViewController.identifier) as! CardViewController
        controller.card = Card(content: content)

        window?.rootViewController = controller
        return true
    }
}

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>

Drag and Drop

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
override func viewDidLoad() {
    super.viewDidLoad()
    collectionView.dragDelegate = self
}

extension BoardCollectionViewController: UICollectionViewDragDelegate {
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        let selectedCard = columns[indexPath.section].cards[indexPath.row]

        let userActivity = selectedCard.userActivity
        let itemProvider = NSItemProvider(object: userActivity)
        
        let dragItem = UIDragItem(itemProvider: itemProvider)
        dragItem.localObject = selectedCard

        return [dragItem]
    }
}

AppDelegate

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 {
    let configurationName: String
    if 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. 🎉

Scene Destruction

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
@IBAction func close(_ sender: Any) {
    guard let session = view.window?.windowScene?.session else { fatalError("No session found for this view controller") }
    let options = UIWindowSceneDestructionRequestOptions()
    options.windowDismissalAnimation = .default
    application.requestSceneSessionDestruction(session, options: options)
}

Where to go from here?

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.