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 3 of 4 of this article. Click here to view the first page.

Saving and Restoring State in Scenes

Providing polished, convenient ways to open content in new windows is important. However, it’s equally important to save and restore the scene’s state to be able to return to it seamlessly.

When a scene moves to the background, the system asks the scene’s delegate for an instance of NSUserActivity to represent its state.

For the best experience, the scene state should not only save the content, but also the visual and interaction state such as scroll and cursor position.

You should save and restore state for all your app’s scenes, but for brevity, you’ll learn how to save and restore the state only for the note creation window.

To make saving and restoring easier, Apple introduced two new methods in UISceneDelegate and its inherited object, UIWindowSceneDelegate.

Open CreateSceneDelegate.swift and add:

func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
  // 1
  guard
    let navigationController = window?.rootViewController 
      as? UINavigationController,
    let noteVC = navigationController.viewControllers.first 
      as? NoteViewController 
  else {
    return nil
  }

  // 2
  let stateActivity = ActivityIdentifier.create.userActivity()

  // 3
  var info: [String: Any] = [
    NoteUserInfoKey.content.rawValue: noteVC.textView.text ?? "",
    NoteUserInfoKey.contentInteractionState.rawValue: 
      noteVC.textView.interactionState
  ]
  if let image = noteVC.selectedImage?.jpegData(compressionQuality: 1) {
    info[NoteUserInfoKey.image.rawValue] = image
  }

  // 4
  stateActivity.addUserInfoEntries(from: info)

  return stateActivity
}

The system calls this method to save the state for a scene. It returns a user activity, which the system gives back to you when you want to restore the state.

Here, you:

  1. Try to find the instance of NoteViewController, which is in the view hierarchy. If there isn’t any, you don’t have anything to save, so return nil.
  2. Create an empty user activity for the note creation page, as you did when you wanted to request a new window.
  3. Store the values of the text and interactionState properties of textView into the userInfo dictionary. interactionState is a new property of UITextField and UITextView on iPadOS 15 that lets you save and restore cursor and scroll position. You also save the image as Data if it’s available.
  4. Add the contents of the info dictionary to the user activity and return it.

To restore the state, implement the method below, extracting the data you saved into the user activity and restoring it in the respective views. Add this method below the method you just added in CreateSceneDelegate.swift:

func scene(
  _ scene: UIScene, 
  restoreInteractionStateWith stateRestorationActivity: NSUserActivity
) {
  // 1
  guard
    let navigationController = window?.rootViewController 
      as? UINavigationController,
    let noteVC = navigationController.viewControllers.first 
      as? NoteViewController,
    let userInfo = stateRestorationActivity.userInfo 
  else {
    return
  }

  // 2
  noteVC.viewType = .create

  // 3
  let image: UIImage?
  if let data = userInfo[NoteUserInfoKey.image.rawValue] as? Data {
    image = UIImage(data: data)
  } else {
    image = nil
  }

  // 4
  let text = userInfo[NoteUserInfoKey.content.rawValue] as? String
  noteVC.textView.text = text ?? ""
  noteVC.selectedImage = image

  // 5
  if let interactionState = 
    userInfo[NoteUserInfoKey.contentInteractionState.rawValue] {
      noteVC.textView.interactionState = interactionState
  }
}

In the code above:

  1. First, you check if the system has finished setting up the view controllers. You also check if there’s any userInfo available to restore.
  2. Next, you set the viewType of NoteViewController to .create. As you may have noticed, NoteViewController is used for both creating and viewing a note.
  3. Then, you check if image data is available inside userInfo. If it’s there and you can create a UIImage from it, you store its image variable.
  4. Next, you set the contents of textView and selectedImage.
  5. Finally, after setting text on UITextView, you set interactionState if it’s available. Always set the interaction state after setting the content.

That’s it. Build and run.

Steps to trigger save and restore in a scene.

Now, follow these instructions to see the save and restore mechanism in action:

  1. Run the app from Xcode.
  2. Tap the plus button.
  3. Add some text and perhaps an image.
  4. Move the cursor to somewhere apart from the end of the text.
  5. Swipe down on the three dots button of the note-creating window to minimize it to the shelf.
  6. Kill the app from Xcode using the Stop button. This will simulate the situation where the system kills the app process.
  7. Run the app again from Xcode.
  8. Tap the New Note window from the shelf.
  9. Everything is there, even the cursor position.

In the next section, you’ll learn about keyboard improvements.

Keyboard Shortcuts Improvements

One characteristic of a Mac app is its Menu Bar, a single place containing every possible action for the app. After Apple started embracing the hardware keyboard for iPad, many people wished for a menu bar on iPad. On iPadOS 15, Apple fulfilled this wish — kind of!

Apps on iPad won’t get a persistent menu bar like Mac apps. Rather, when you hold Command on the hardware keyboard connected to the iPad, you’ll get a new menu system that looks similar to the Mac implementation.

Here are some of the features of this new system:

  1. Apps can categorize actions into groups.
  2. Users can search for available actions, just like on macOS.
  3. The system automatically hides inactive actions instead of disabling them.
  4. The API is similar to the one used to create menu items for a Catalyst app. As a result, you don’t need to duplicate things when adding keyboard shortcuts for iPad and Mac Catalyst.

In NotesLite, there are a couple of keyboard shortcuts available.

Specifically, NoteViewController contains Save and Close actions triggered by Command-S and Command-W. In NotesListViewController, you can create a new note by pressing Command-N.

See the shortcut action groups available right now in NotesLite by holding the Command key:

Uncategorized keyboard shortcuts

The category for the action is the name of the app. When the developers of an app use the old mechanism for providing keyboard shortcuts, this is how it looks. Next, you’ll update to the modern approach.

Updating to the Menu Builder API

One of the old ways of adding keyboard shortcuts support was overriding the keyCommands property of UIResponder. Since UIViewController is a UIResponder, you can do this in view controllers.

There are two occurrences of keyCommands in NotesLite. In NoteViewController.swift, you’ll see:

override var keyCommands: [UIKeyCommand]? {
  [
    UIKeyCommand(title: "Save", action: #selector(saveNote), 
      input: "s", modifierFlags: .command),
    UIKeyCommand(title: "Close", action: #selector(dismiss), 
      input: "w", modifierFlags: .command)
  ]
}

Remove keyCommands from NotesListViewController.swift and NoteViewController.swift. You can use Xcode’s Find feature.

Apple recommends defining all menu items for your app at launch. To do so, open AppDelegate.swift.

Override buildMenu(with:), which is a method on UIResponder:

override func buildMenu(with builder: UIMenuBuilder) {
  super.buildMenu(with: builder)

  // 1
  guard builder.system == .main else { return }

  // 2
  let newNoteMenu = UIMenu(
    options: .displayInline,
    children: [
      UIKeyCommand(
        title: "New Note",
        action: #selector(NotesListViewController.openNewNote),
        input: "n",
        modifierFlags: .command)
    ])

  // 3
  let saveMenu = UIMenu(
    options: .displayInline,
    children: [
      UIKeyCommand(
        title: "Save",
        action: #selector(NoteViewController.saveNote),
        input: "s",
        modifierFlags: .command)
    ])

  // 4
  let closeMenu = UIMenu(
    options: .displayInline,
    children: [
      UIKeyCommand(
        title: "Close",
        action: #selector(NoteViewController.dismiss),
        input: "w",
        modifierFlags: .command)
    ])

  // 5
  builder.insertChild(newNoteMenu, atStartOfMenu: .file)
  builder.insertChild(closeMenu, atEndOfMenu: .file)
  builder.insertChild(saveMenu, atEndOfMenu: .file)
}

In the code above, you:

  1. Check if the system is calling the menu builder API for the main menu bar.
  2. Create UIMenu instances for all items you want in the menu bar. Here, you’re creating a menu item called New Note with the keyboard shortcut Command-N. The selector for this action is openNewNote() inside NotesListViewController.
  3. Make a menu item for saving a note. This time, the trigger is inside NoteViewController.
  4. Create a menu item for closing the note window.
  5. Put menu items in various system-defined groups, such as File and Edit. You can create a new category if you desire.

Build and run. Tap the plus button or press Command-N, and then hold the Command key.

Categorized keyboard shortcuts for note creation window

The system even added text editing shortcuts under the Edit menu for free. Who doesn’t like free stuff?

Note: If the shortcuts don’t appear, make sure you’re returning true in application(_:didFinishLaunchingWithOptions:) in AppDelegate.

Conditionally Disabling Certain Actions

There’s a small issue, though. What if you want to conditionally disable certain actions? For instance, the Save action doesn’t make sense when the NoteViewController isn’t in create mode.

To resolve this, override another UIResponder method called canPerformAction(_:withSender:). When you return true here, the action works; otherwise, it’ll get ignored. Add this method inside NoteViewController right after viewDidLoad():

override func canPerformAction(
  _ action: Selector, 
  withSender sender: Any?
) -> Bool {
  if action == #selector(dismiss) { // 1
    return splitViewController == nil
  } else if action == #selector(saveNote) { // 2
    return viewType == .create
  } else { // 3
    return super.canPerformAction(action, withSender: sender)
  }
}

In the code above:

  1. The system calls this any time a selector reaches this view controller in the responder chain. As a result, you need to check for action to act based on the input. If it’s the dismiss selector, return true only if splitViewController is nil. If you presented this page inside a new window, there would be no UISplitViewController involved. Pressing Command-W will kill the app if you don’t do this check.
  2. If the action is saveNote, check whether this view controller is in create mode.
  3. Otherwise, let the system decide.

Build and run.

Hiding unrelated keyboard shortcuts in note detail page

Open a note in a new window, and hold the Command key. This time, the Save action isn’t there anymore.