iPadOS 15 Tutorial: What’s New for Developers

See what’s new in iPadOS 15 and take your app to the next level with groundbreaking changes! By Saeed Taheri.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Activation Action

Per Apple’s guidelines, you only need to open new windows for your app based on the user’s explicit interaction. As you can implement many of these interactions using UIAction, Apple provided a code shortcut.

In NotesListViewController.swift, go to configureBarButtonItems(). Then, create an action that calls openNewNote(), and attach it to the bar button item.

Do this by replacing the current configureBarButtonItems() with this:

private func configureBarButtonItems() {
  // 1
  let addAction = UIAction { _ in
    let navigationController = UINavigationController(
      rootViewController: NoteViewController.storyboardInstance)
    self.present(navigationController, animated: true)
  }

  // 2
  let newSceneAction = UIWindowScene.ActivationAction(
    alternate: addAction
  ) { _ in
    // 3
    let userActivity = ActivityIdentifier.create.userActivity()

    let options = UIWindowScene.ActivationRequestOptions()
    options.preferredPresentationStyle = .prominent

    // 4
    return UIWindowScene.ActivationConfiguration(
      userActivity: userActivity, 
      options: options)
  }

  // 5
  navigationItem.rightBarButtonItem = UIBarButtonItem(
    systemItem: .add,
    primaryAction: newSceneAction,
    menu: nil)
}

Here’s what this does:

  1. First, create a UIAction that presents NoteViewController modally.
  2. Next, create an instance of UIWindowsScene.ActivationAction. As the name implies, you use it for activating a scene. Pass the addAction you created in step 1 as a parameter to this function. UIKit automatically runs the alternate action when the device doesn’t support multiple windows. How convenient is that?
  3. Then, create a user activity for the note creation scene and configure the request options. You’re already familiar with this step.
  4. Here, you return an instance of UIWindowScene.ActivationConfiguration, passing the user activity and options. It’s like when you passed these items to requestSceneSessionActivation(_:userActivity:options:errorHandler:).
  5. Since newSceneAction is actually an instance of UIAction, you set it as the primary action of the bar button item.

Build and run. Then, try tapping the plus icon. If nothing changes, it means you were successful.

Activation Interaction

While on iPadOS 14 and below, Apple insisted on Drag & Drop as the way to open a new window, on iPadOS 15, it advertises context menus and a new pinch open gesture. Apple also integrated these in its own apps. For instance, open the Notes app.

In the sidebar, you can touch and hold or right-click with a mouse or trackpad to open the context menu. Choosing Open In New Window will open a note in a new window with the prominent style you saw earlier.

Open In New Window option in Notes app context menu.

You can also pinch open with two fingers on any item in the sidebar to open it in a new window, prominently.

Next, you’ll add these options to NotesLite.

Context Menu

In NotesListViewController.swift, scroll to the mark line // MARK: - UICollectionViewDelegate.

Look at collectionView(_:contextMenuConfigurationForItemAt:point:). This method adds context menu items for each row. For now, it only contains delete. You’ll add a new action for opening the note in a new window.

First, though, you need to create a helper method for configuration, which you’ll use in the next step. Add this inside NotesListViewController just below the definition of `deleteItem(at:)`:

private func activationConfiguration(
  for indexPath: IndexPath
) -> UIWindowScene.ActivationConfiguration? {
  // 1
  guard let note = dataSource.itemIdentifier(for: indexPath) else {
    return nil
  }
  // 2  
  var info: [String: Any] = [
    NoteUserInfoKey.id.rawValue: note.id,
    NoteUserInfoKey.content.rawValue: note.content
  ]

  // 3
  if let data = note.image?.jpegData(compressionQuality: 1) {
    info[NoteUserInfoKey.image.rawValue] = data
  }

  // 4
  let userActivity = ActivityIdentifier.detail.userActivity(userInfo: info)

  let options = UIWindowScene.ActivationRequestOptions()
  options.preferredPresentationStyle = .prominent

  let configuration = UIWindowScene.ActivationConfiguration(
    userActivity: userActivity,
    options: options)
  return configuration
}

It looks rather long; however, it’s pretty straightforward:

  1. Get the note pertaining to the indexPath from the collectionView‘s dataSource. It may return nil, so use guard-let syntax and exit the method early if the index is nil.
  2. The way to pass data to the system for creating a new window is through user activities. Each user activity has userInfo, in which you can store property list data. Since userInfo uses a string-based key-value dictionary, decrease possible errors by using some predefined keys, which are inside the starter project. Here, you store the note’s id and content.
  3. Check if the note has an associated image. If so, compress it to JPEG and save it to userInfo as Data.
  4. Like before, create a user activity, set the request options and return a configuration made with them.

Now, return to // MARK: - UICollectionViewDelegate and replace let actions = [delete] with the following:

// 1
var actions = [delete]

// 2
if let configuration = self.activationConfiguration(for: indexPath) {
  // 3
  let newSceneAction = UIWindowScene.ActivationAction { _ in
    return configuration
  }
  
  // 4
  actions.insert(newSceneAction, at: 0)
}

In the code above, you:

  1. Change actions from a let to a var, so you can add items later.
  2. Get an instance of UIWindowScene.ActivationConfiguration using activationConfiguration(for:), which you’ll write later. Since it may be nil in certain cases, you conditionally unwrap it.
  3. Create a new activation action as you did earlier, and then return the configuration you got from step 2.
  4. Insert newSceneAction at the top of actions.

As in the original code, this returns a menu using the specified actions.

Build and run. Invoke the context menu in the notes list by touching and holding or right-clicking. You may now open the note in a new window.

Open In New Window option in NotesLite context menu.

Note detail page opened in a new window prominently.

Next, you’ll add pinch support on UICollectionView items.

Pinching

First, implement a new delegate method. Add this at the end of NotesListViewController.swift, just before the closing brace:

override func collectionView(
  _ collectionView: UICollectionView,
  sceneActivationConfigurationForItemAt
  indexPath: IndexPath,
  point: CGPoint
) -> UIWindowScene.ActivationConfiguration? {
  activationConfiguration(for: indexPath)
}

You return an activation configuration for each item you’d like to support pinching.

Build and run. Then, try pinching open on a note.

Pinch on a note in the sidebar

The entire row gets bigger while you pinch. You can customize the transition in a way that only the image scales up. To do this, tell the system on which view the scale transition should occur.

Open activationConfiguration(for:), and right before the return configuration line, add:

// 1
if let cell = collectionView.cellForItem(at: indexPath) {
  // 2
  if let imageView = cell.contentView.subviews.first(
    where: { subview in
      (subview as? UIImageView)?.image != nil
    }
  ) {
    // 3
    configuration.preview = UITargetedPreview(view: imageView)
  }
}

Here’s what this does:

  1. First, get the cell the user pinched.
  2. Find the imageView inside the subviews of the cell’s contentView where image isn’t nil.
  3. Set the imageView you found in step 2 as the preview of the activation configuration.

Build and run. Try pinching one more time. It looks much more polished.

Pinch on the note in the sidebar. Transition begins from the image.

Note: To support this pinch gesture on views other than cells in a UICollectionView, create a UIWindowScene.ActivationInteraction and attach it to a custom view anywhere in the hierarchy. It’s easy to do, but beyond the scope of this tutorial.