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
While tablets don’t seem high priority for most platform or device makers, Apple never stops improving iPad’s hardware and software. In 2019, Apple renamed the tablet’s operating system iPadOS, emphasizing its unique multitasking and keyboard support. Continuing Apple’s two-year cycle of considerable iPadOS updates, iPadOS 15 offers many improvements.
In this tutorial, you’ll learn about enhancements in iPadOS 15, including:
- Multitasking improvements
- Keyboard shortcuts improvements
- Pointer updates
You’ll do this while modernizing the app NotesLite, which you can use to write notes and add images.
Getting Started
Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.
The starter project contains a fully functional app with features introduced for iPadOS 14 and below. You’ll improve the app by adding new features introduced in iPadOS 15 and making it look more modern and functional.
Multiple Windows or Scenes
In iPadOS 13, Apple introduced the concept of scenes to support multiple windows in iPad apps.
Windows are represented by UIScene
instances. A UISceneSession
manages a scene. Throughout this tutorial, when you see a scene or UISceneSession
, you can think of it as a window.
If you built an app before iOS 13, you know AppDelegate
is the one place that does everything related to app launch, foregrounding, backgrounding and more.
In iOS 13, Apple moved some of AppDelegate
‘s responsibilities to SceneDelegate
.
Nowadays, application entry points are inside AppDelegate
, and window-related stuff — such as backgrounding, foregrounding and URL handling — are inside an object conforming to UISceneDelegate
or UIWindowSceneDelegate
.
These also apply to iPhone apps. You can think of an iPhone app as a single window application.
Each scene needs an instance of UISceneConfiguration
for its configuration. You define these configurations inside Info.plist.
Now, you’ll see how all these connect inside the starter project.
Exploring NotesLite
Open the starter project and choose an iPad target. Then, build and run.
The image above shows how the app works out of the box.
Tap the plus button so a new window appears on the side. Write your note, add a picture and tap Save. The creation window closes and the newly added note appears in the sidebar. Tapping the note in the sidebar will show the detail page on the right.
The app already supports multiple windows. Take a look at the file structure:
Inside the Scenes folder are the three subclasses for UIWindowSceneDelegate
:
- First, there’s
SceneDelegate
for the default window of the app. - Then, there’s
CreateSceneDelegate
for the note creation window. - Finally, there’s
DetailSceneDelegate
for the note detail window.
When you opened the app, the default window appeared. After tapping the plus button, CreateSceneDelegate
took over. You’ll add support for a detail window later in the tutorial.
Inside the Supporting Files folder, open Info.plist.
There’s a key called Application Scene Manifest whose value is already in the starter project. You need to define each scene configuration your app supports inside this key.
As you can see in the screenshot above, you need to define at least the Configuration Name and the Delegate Class Name to which this configuration relates.
NSUserActivity
In iOS 8, Apple introduced a class called NSUserActivity
. At first, you could use this class to integrate the Handoff feature between devices.
Each year, this class became more powerful. There’s even a running joke in the community that Apple might one day deprecate all iOS APIs and release everything under NSUserActivity
‘s tent.
As of iOS 15, you can — and should — use NSUserActivity
if you support any of these features:
- Handoff
- In-app Spotlight search
- Siri and Shortcuts Intents
- Multiple windows on iPad
When you want to open a new window inside your app, ask the system to create a scene using requestSceneSessionActivation(_:userActivity:options:errorHandler:)
on UIApplication
‘s shared
object.
However, you can’t directly specify a scene name here. You do this using NSUserActivity
. The system will give you back this instance, and you can decide which scene to configure and present.
Window Presentation Style
On iPadOS 14 and earlier, the user interface for managing an app’s windows was so hidden and difficult to use that even some pro users avoided it. Fortunately, on iPadOS 15, this UI is much better.
At the top of each app that supports Split View and Slide Over, there’s a new button represented by three dots. With NotesLite open, tap it.
Three buttons appear. This lets you put the window in Full Screen, Split View or Slide Over without going through the hassle of dragging and dropping. Hurray!
However, these aren’t the only options available. In iPadOS 15, Apple added a new style to the window modes: Prominent.
It looks a like a Form Sheet at first, but you can easily put it in any other mode. Now, it’s time to add it to the app.
Open NotesListViewController.swift inside the UI group. Go to openNewNote()
. Here’s what it looks like:
@objc func openNewNote() {
// 1
if UIDevice.current.userInterfaceIdiom == .pad {
// 2
let userActivity = ActivityIdentifier.create.userActivity()
// 3
let options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .standard
// 4
UIApplication.shared.requestSceneSessionActivation(
nil,
userActivity: userActivity,
options: options,
errorHandler: nil)
} else {
let navigationController = UINavigationController(
rootViewController: NoteViewController.storyboardInstance)
present(navigationController, animated: true)
}
}
Here’s what this does:
- Since iPhone apps don’t support multiple scenes, partition based on the device the code is running on.
- Create a
userActivity
using a helper method from SceneConfigurations.swift. - Next, provide the system with some activation options. The system tries to consider these requests when activating a scene. This code asks the system to show a
standard
presentation style. This style is what made the creation window appear alongside the main window. On iPadOS 15, this option defaults toautomatic
, and the system decides what works best. - Request a scene activation with the user activity and the request options. This makes the new window appear.
Now, change the preferred presentation style line to this:
options.preferredPresentationStyle = .prominent
Build and run. Then, tap the plus button.
A new window appears on top of the current view.
There’s a tiny indicator that shows this isn’t a form sheet or a usual modal presentation: the three dots button on the top.
Tap it, and you’ll see a new option:
The icon speaks for itself. You asked the system to present this window prominently.
The three dots button does another job, too. This time, instead of tapping it, try swiping it down. If you look closely, the window goes somewhere at the bottom of the screen. This area is the App Shelf. It’s a place where you can see all open windows for an app and switch between them.
If an app has multiple active windows, when you open it from Home Screen, the shelf appears for a split second. You can also summon the shelf at any time by tapping on the three dots button. Close the windows from the shelf by swiping up.
Here’s a GIF to illustrate these interactions:
Next, you’ll learn about activation actions.
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:
- First, create a
UIAction
that presentsNoteViewController
modally. - Next, create an instance of
UIWindowsScene.ActivationAction
. As the name implies, you use it for activating a scene. Pass theaddAction
you created in step 1 as a parameter to this function.UIKit
automatically runs thealternate
action when the device doesn’t support multiple windows. How convenient is that? - Then, create a user activity for the note creation scene and configure the request options. You’re already familiar with this step.
- Here, you return an instance of
UIWindowScene.ActivationConfiguration
, passing the user activity and options. It’s like when you passed these items torequestSceneSessionActivation(_:userActivity:options:errorHandler:)
. - Since
newSceneAction
is actually an instance ofUIAction
, 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.
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:
- Get the
note
pertaining to theindexPath
from thecollectionView
‘sdataSource
. It may returnnil
, so useguard-let
syntax and exit the method early if the index isnil
. - 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. SinceuserInfo
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’sid
andcontent
. - Check if the note has an associated image. If so, compress it to JPEG and save it to
userInfo
asData
. - 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:
- Change
actions
from alet
to avar
, so you can add items later. - Get an instance of
UIWindowScene.ActivationConfiguration
usingactivationConfiguration(for:)
, which you’ll write later. Since it may benil
in certain cases, you conditionally unwrap it. - Create a new activation action as you did earlier, and then return the configuration you got from step 2.
- Insert
newSceneAction
at the top ofactions
.
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.
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.
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:
- First, get the cell the user pinched.
- Find the
imageView
inside the subviews of the cell’scontentView
whereimage
isn’tnil
. - Set the
imageView
you found in step 2 as thepreview
of the activation configuration.
Build and run. Try pinching one more time. It looks much more polished.
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.
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.
Pointer Improvements
Apple introduced pointer support in iPadOS 13.4. This year, it got its first set of enhancements.
Band Selection
The first addition is band selection, a new pointer-specific multi-selection experience familiar to Mac users.
In iPadOS 15, when you click and drag in a non-list UICollectionView
, the pointer stretches into a rectangle, and the collection view selects the items the rectangle encompasses.
Any UICollectionView
that supports the existing one and two-finger multi-selection gestures via the shouldBeginMultiple
Selection Interaction API gets this behavior automatically in iPadOS 15.
For anything apart from a UICollectionView
, the new UIBandSelectionInteraction
API allows you to easily adopt this experience.
Here’s a GIF from the Files app:
Pointer Accessories
The second addition to the system pointer is the ability to attach accessories.
In iPadOS 14 and earlier, you could provide a custom shape for the pointer if you desired. However, for most use cases, you only need to add certain accessories around the system pointer.
If you look closely at the note detail page, there’s a handle at the bottom of the image. If you touch and drag it, you can resize the image. You’ll add accessories to the pointer, so it’s clearer that you can resize the image vertically.
In NoteViewController.swift, find dragHandler
. At the end of the didSet
block, add:
let interaction = UIPointerInteraction(delegate: self)
dragHandler.addInteraction(interaction)
This creates a new pointer interaction, sets the NoteViewController
as its delegate and adds it to the interactions list of dragHandler
.
To silence the compiler’s nagging, at the end of the current file, add this:
extension NoteViewController: UIPointerInteractionDelegate {
// 1
func pointerInteraction(
_ interaction: UIPointerInteraction,
styleFor region: UIPointerRegion
) -> UIPointerStyle? {
// 2
let preview = UITargetedPreview(view: dragHandler)
// 3
let style = UIPointerStyle(effect: .lift(preview))
// 4
style.accessories = [
.arrow(.top),
.arrow(.bottom)
]
// 5
region.latchingAxes = .vertical
return style
}
}
In the code above, you:
- Override
pointerInteraction(_:styleFor:)
. The system consults this method for a pointer’s style on a certain view. - Create a targeted preview with
dragHandler
. You know this API since you used it to customize the pinch transition. - Create a pointer-style object with the
lift
effect. Other options arehighlight
andhover
. Lift looks best for this interaction. - Add accessories around the pointer. Here, you added two arrows to the top and bottom of the pointer. You’re not limited to this, though. One can use a custom shape with a custom position.
- Having the ability to set
latchingAxes
is new this year. When set, the style associated with this region will lock in and allow free-form movement along the specified axes.
Finally, build and run. If you’re testing in the simulator, select Input ▸ Send Pointer to Device from the I/O menu.
Look how cool the pointer interaction is!
Where to Go From Here?
You can download the completed project files by clicking Download Materials at the top or bottom of this tutorial.
While you’ve done a lot today, iPadOS 15 is a solid release and there’s more to learn.
Here are some places to consult:
- The iPadOS Multitasking video course on raywenderlich.com.
- The Pointer Interaction Tutorial on raywenderlich.com.
- Apple WWDC sessions, including Take your iPad apps to the next level.
- Apple sample projects from WWDC, like Focus on iPad keyboard navigation.
We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!