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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iPadOS 15 Tutorial: What’s New for Developers
30 mins
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:
- 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 returnnil
. - Create an empty user activity for the note creation page, as you did when you wanted to request a new window.
- Store the values of the
text
andinteractionState
properties oftextView
into theuserInfo
dictionary.interactionState
is a new property ofUITextField
andUITextView
on iPadOS 15 that lets you save and restore cursor and scroll position. You also save theimage
asData
if it’s available. - 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:
- First, you check if the system has finished setting up the view controllers. You also check if there’s any
userInfo
available to restore. - Next, you set the
viewType
ofNoteViewController
to.create
. As you may have noticed,NoteViewController
is used for both creating and viewing a note. - Then, you check if image data is available inside
userInfo
. If it’s there and you can create aUIImage
from it, you store itsimage
variable. - Next, you set the contents of
textView
andselectedImage
. - Finally, after setting
text
onUITextView
, you setinteractionState
if it’s available. Always set the interaction state after setting the content.
That’s it. Build and run.
Now, follow these instructions to see the save and restore mechanism in action:
- Run the app from Xcode.
- Tap the plus button.
- Add some text and perhaps an image.
- Move the cursor to somewhere apart from the end of the text.
- Swipe down on the three dots button of the note-creating window to minimize it to the shelf.
- Kill the app from Xcode using the Stop button. This will simulate the situation where the system kills the app process.
- Run the app again from Xcode.
- Tap the New Note window from the shelf.
- 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:
- Apps can categorize actions into groups.
- Users can search for available actions, just like on macOS.
- The system automatically hides inactive actions instead of disabling them.
- 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:
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:
- Check if the system is calling the menu builder API for the
main
menu bar. - 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 isopenNewNote()
inside NotesListViewController. - Make a menu item for saving a note. This time, the trigger is inside NoteViewController.
- Create a menu item for closing the note window.
- 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.
The system even added text editing shortcuts under the Edit menu for free. Who doesn’t like free stuff?
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:
- 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 thedismiss
selector, returntrue
only ifsplitViewController
isnil
. If you presented this page inside a new window, there would be noUISplitViewController
involved. Pressing Command-W will kill the app if you don’t do this check. - If the action is
saveNote
, check whether this view controller is increate
mode. - Otherwise, let the system decide.
Build and run.
Open a note in a new window, and hold the Command key. This time, the Save action isn’t there anymore.