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
textandinteractionStateproperties oftextViewinto theuserInfodictionary.interactionStateis a new property ofUITextFieldandUITextViewon iPadOS 15 that lets you save and restore cursor and scroll position. You also save theimageasDataif it’s available. - Add the contents of the
infodictionary 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
userInfoavailable to restore. - Next, you set the
viewTypeofNoteViewControllerto.create. As you may have noticed,NoteViewControlleris 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 aUIImagefrom it, you store itsimagevariable. - Next, you set the contents of
textViewandselectedImage. - Finally, after setting
textonUITextView, you setinteractionStateif 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
mainmenu bar. - Create
UIMenuinstances 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
actionto act based on the input. If it’s thedismissselector, returntrueonly ifsplitViewControllerisnil. If you presented this page inside a new window, there would be noUISplitViewControllerinvolved. 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 increatemode. - 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.



