Streaming is available in most browsers,
and in the Developer app.
-
Build a desktop-class iPad app
Discover how you can create iPad apps that take advantage of desktop class features. Join Mohammed from the UIKit team as we explore the latest navigation, collection view, menu, and editing APIs and learn best practices for building powerful iPad apps. Code along with this session in real time or download our sample app to use as a reference for updating your own code.
Resources
- Building a desktop-class iPad app
- centerItemGroups
- collectionView(_:contextMenuConfigurationForItemsAt:point:)
- collectionView(_:performPrimaryActionForItemAt:)
- Supporting desktop-class features in your iPad app
- titleMenuProvider
- UIDocumentProperties
- UINavigationItem.ItemStyle
- UINavigationItemRenameDelegate
Related Videos
WWDC23
WWDC22
-
Download
♪ ♪ Mohammed: Hi, I'm Mohammed from UIKit. Thank you for joining me for this deep dive into building a Desktop Class iPad app. In this video, we'll use iPadOS 16 APIs to update an existing iPad app to a desktop class experience. We'll start by using new navigation bar API to surface powerful functionality, increase UI density, and provide customizability.
Then, we'll adopt new UICollectionView and menu API to enable complex workflows and quick actions on multiple selections. And we'll round things out by enabling the new Find and Replace experience and enhancing text editing with the new Edit Menu. The app we'll be updating is a Markdown editor built for iPadOS 15. As we walk through each step of the modernization process, I'll discuss best practices and motivations behind our choices, giving you an idea of the factors you should consider while taking your own app through a similar process.
If you'd like a bit of a primer before getting started, check out “Meet Desktop Class iPad” for a breakdown of all of UIKit's new iPadOS APIs, and check out “What's new in iPad app design” for tips on how to design the best Desktop Class iPad app possible. All right, let's dive right in! To start things off, let's consider the organization of our app's controls. Since the app is designed for iPadOS 15, it already exposes its most important controls in the navigation bar and places secondary controls in various menus and popovers.
In iPadOS 16, UIKit formalizes the existing navigation style and introduces two new ones with a denser and more customizable layout. This allows apps to express the layout that's most appropriate to their content while bringing more functionality to the forefront of the UI.
Navigator apps have a familiar push / pop navigation model. This is generally appropriate for apps that display hierarchical data, like Settings.
Browsers, like Safari or Files are ideal for looking through and navigating back and forth between multiple documents or folder structures.
And Editors are great for focused viewing or editing of individual documents.
Being a Markdown editor, this style is the perfect choice for our app.
The editor style aligns the title towards the leading edge of the bar, opening up its center for a new set of items. This'll allow us to expose additional functionality that might've been hidden away in other views or menus. We're going to do a few things to make as much use of this design as possible. We'll start by customizing the built-in back action to fit our needs. Then we'll add a title menu with some document info and common document actions. We'll also add support for renaming via the new built-in rename UI. And finally, we'll make previously buried functionality more easily accessible by bringing it to the center of the bar. Let's start by opting into the editor style by setting our view controller's navigationItem's style property to .editor.
This immediately gives us the leading aligned title and opens up the center area.
After that, let's remove our trailing done button and replace it using the new backAction API. This way we get a more standard look for the action that dismisses this view and returns to the document picker.
Next let's figure out whether our app would benefit from a title menu. As the name implies, the title menu is presented from the navigation bar's title view. It's a great place to show document metadata and surface actions that apply to the whole document. If your app isn't document based, it may be a good place to surface actions that apply to the entire view.
For our app, it makes sense to use the document menu's header to surface some useful information about the document. We'll also be able to provide a draggable representation of the document and easy access to sharing functionality. And now it's time to write some code! Our app is UIDocument backed, so we can use the UIDocument's fileURL to instantiate a UIDocumentProperties object.
Next, we'll use that same URL to create an NSItemProvider.
Then we'll use the item provider to create a UIDragItem which we'll return from the properties object's dragItemsProvider.
We'll also use it to construct a UIActivityViewController which we'll return from the properties object's activityViewControllerProvider. And finally, we'll set the properties object as the editor view controller's navigationItem's documentProperties. The code we just wrote results in this document header, which provides a quick overview of the document including its name, size, and an icon representation. Since we specified drag item and activity view controller providers, I can drag the icon to copy the document outside the app or tap the share button to bring up an activity view controller.
In addition to displaying the document header, the title menu is a good place to provide functionality that applies to the entire document. There are two kinds of actions that can be displayed in this menu: system-provided ones with pre-defined localized titles and symbol images, and entirely custom ones that the app provides.
Since it comes with some additional behavior, let's start with the rename action. We can add this action to our menu by conforming to the rename delegate protocol. When triggered, the action presents the bar's built-in rename UI.
First we'll assign our view controller as its navigation item's renameDelegate.
Then, we'll implement navigationItemDidEndRenamingWithTitle to handle the actual renaming of the displayed document.
This function is called when the rename action is committed. It's the app's responsibility to handle this by actually renaming the document. The API is intentionally open ended to support any kind of data model your app may have. Moving on to other system provided actions, we'll first need to override their functions on our editor view controller. Here we've implemented the duplicate and move functions. UIKit automatically surfaces system-provided actions, including the rename action, in the navigationItem's titleMenuProvider as an array of suggested UIMenuElements. To include them in our title menu, we'll just add them to the returned menu's children.
In addition to the system-vended actions, we can add entirely custom actions or even whole menu hierarchies. Here I've added an Export submenu with export as HTML and PDF sub-actions.
And with that, tapping the title view now brings up a menu with the document header and all the actions we just added. And when I select rename, the built-in rename UI is activated and I'm able to rename the document.
Now that we've started establishing the base structure of our app, it's a good time to check in on how things look when we build our app with Mac catalyst. When we run the app on a Mac, we'll find that the editor style with its leading aligned title has been translated nicely.
Our back action has also been carried over, and when clicked, brings up a file browser.
The system provided actions and rename functionality are automatically surfaced in the app's File menu. Note that the titleMenuProvider is not called on Mac Catalyst, so our custom actions are not included in the File menu. To expose these actions, we would need to manually add them to the app's main menu using the main UIMenuSystem.
All right, let's continue our modernization process. We'll keep checking in on the Mac as we make progress towards our goal. Let's consider the opportunities made available by the bar's center area. The iOS 15 version of the app has a menu that holds a number of secondary controls and tools. With center items, we're able to make these tools more discoverable.
Since the center area is customizable, we can include a large set of controls without worrying about filling the UI with less commonly used ones. Each person can tailor the bar's contents to fit their workflow. The first step in enabling customization is specifying a customizationIdentifier on the navigation item.
Next, we'll define the center items as UIBarButtonItemGroups. Groups are an existing concept that's been extended to UINavigationBar and enhanced to support customization in iOS 16. This screenshot shows the set of center items we'd like to show by default. The synchronize scrolling button all the way on the left provides an important function that can't be reached by any other means, so it makes sense to place it in a fixed group using UIBarButtonItem's new creatingFixedGroup() function. Fixed groups are not customizable and cannot be moved by the user.
The add link button, on the other hand, doesn't provide critical functionality, and the same task can be achieved by typing link tags in the editor, so we'll use creatingOptionalGroup to create a completely customizable item. And we'll give it a unique customizationIdentifier so the customization is persisted across app launches.
We'll follow a similar process to define the remaining items in the default set, then move on to lower priority items that don't need to be available by default. One such item is the text format group, which includes the bold, italics, and underline items.
It's not important enough to show by default, but we want it in the customization popover so it can be dragged into the bar.
To achieve this, we'll use UIBarButtonItemGroup's optionalGroup initializer with isInDefaultCustomization set to false.
We'll also be sure to give the group a representative item so it has a title in the popover, and has a compact representation that it can be collapsed to when the bar runs out of space.
Back on the iPad, the center items we defined show up in the center of the bar. If I click the newly added More button, a menu shows up with a Customize Toolbar action. And if I click that, the customization mode is activated.
The sync scrolling button that we marked as fixed is de-emphasized and static, while all the other items lift and shake to show that they're customizable.
Optional items like the Format group show up in the popover and can be dragged into the bar.
When we run the app on a Mac, we find that the center items have been converted to fully customizable macOS toolbar buttons.
Before we move on, let's go back to the iPad for a minute and resize the app. Now that we have less space available in the toolbar, the center items are no longer visible. UIKit automatically handles showing and hiding center items in response to the available space. Any items that don't fit are displayed in the overflow menu. Standard bar button items are automatically converted to their menu representation, but we're also able to provide a custom menu representation if we like. Since UIKit has no insight into the purpose of a custom view item, our slider item isn't automatically translated. We'll need to manually specify a menu representation.
Here's our slider item. It's a single bar button item with a custom view, wrapped in an optional bar button group. To provide the core functionality of the slider, we'll define the menu representation as a UIMenu with Decrease, Reset, and Increase actions.
Using UIMenu's new preferredElementSize property, we can give the menu a more compact side by side appearance.
And using the new keepsMenuPresented attribute, we can keep the menu presented after each action is performed, allowing the font size to be changed multiple times without dismissing and re-presenting the menu. Let's run this on the iPad again. Now when we bring up the overflow menu, the slider appears as an inline menu with three side-by-side actions, covering the full functionality of the slider.
Since the small element size doesn't exist on the Mac, the actions will appear as standard macOS menu items. And that's it for UI organization and customization. Next, let's look into speeding up some workflows in the app using new collection view and menu API. Our app has a table of contents sidebar that can be used to quickly navigate the document or take action on top level tags. Prior to iOS 16, adding the ability to edit multiple items would've likely meant implementing a distinct edit mode, with bulk actions relegated to buttons in a toolbar.
iOS 16 introduces a new design for multi-item menus with a flock of items that clearly communicates which items the menu affects and provides a direct transition to a multi-item drag. In a desktop class iPad app, this new menu design is best paired with a lighter weight selection style. "Lightweight" here means selecting multiple items without kicking the collection view into an edit mode or making significant changes to the app's UI. We can achieve this and enable keyboard focus using existing API. First, we'll set allowsMultipleSelection to true.
Then we'll enable keyboard focus by setting allowsFocus to true.
And we'll allow focus to drive selection by setting selectionFollowsFocus to true.
If we run this on our iPad, we immediately notice that as each item is added to the selection, it still fires its selection action, causing the editor view to scroll. Let's head back to our code and figure out what's going on.
There it is! The code in didSelectItemAtIndexPath tries to disallow scrolling while in edit mode by checking the collectionView's isEditing property. Now that we allow multiple selection outside of edit mode, this code runs for every selection. We can fix this using a new UICollectionViewDelegate method. We'll implement performPrimaryActionForItemAtIndexPath and just move our scrolling code to this new function. Since this function is only called when a single item is tapped and the collection view is not editing, we no longer need the check for edit mode.
And since we don't have any selection related behavior, we can remove our implementation of did select item at indexPath.
Back on the iPad, selecting multiple items no longer scrolls to the corresponding text in the editor view. With that done, let's actually add support for the menu.
In iPadOS 16, UICollectionViewDelegate's existing single item menu method is deprecated. Its replacement supports displaying menus for anywhere from zero to many items. The number of items in the given indexPaths array depends on how many items are selected, and where the menu is invoked.
If the array is empty, then the menu was invoked in the blank space between cells.
If it has a single indexPath, then it was invoked on an item that is either deselected or is the sole selected one.
If it has more than one item, then the menu was invoked on an item that is part of a multiple selection.
If I head back to the iPad, select the top four items again, and two-finger click one of the selected items, a new multi-item menu comes up.
When I do the same thing on a Mac, a ring is drawn around the selected cells to highlight them.
With multi-item menus done, let's look into enhancing the text editing experience using the new Find and Replace and edit menu features. Our app uses a UITextView for its editor and doesn't require any custom Find and Replace behavior, so all we need to do to enable the default system functionality is set the text view's isFindInteractionEnabled property to true. With that set, hitting Command+F while editing text brings up the Find and Replace UI.
Adding custom actions to the text view's edit menu doesn't take much, and can enable some great quick editing features. We'll just implement the new UITextViewDelegate method edit menu for text in range suggested actions. In the implementation, we can construct and return a UIMenu that combines custom actions, like this Hide action, with the system menu.
And this is the result. W hen I select some text and bring up the edit menu, both our custom actions and the system-provided ones are displayed. For more information about Find and Replace and the edit menu, check out “Adopt desktop class editing interactions.” And that's it! With these few changes, we've taken some great basic steps towards making our app desktop class and translating it seamlessly to the Mac. Use the APIs offered in iPadOS 16 to take your own app through a similar process. Start by choosing a navigation style that fits your app. Enhance document workflows with document properties and the title menu. And surface important functionality and provide customizability with center items. Enable quickly acting on multiple items with multi-item menus. And enhance your app's text editing experience using Find and Replace and the new edit menu. Whether you're building a new app or updating an existing one, I can't wait to use the apps you build with these new tools. Thanks for watching.
-
-
3:36 - Enable UINavigationBar editor style.
navigationItem.style = .editor
-
3:52 - Set a back action.
navigationItem.backAction = UIAction(…)
-
4:48 - Create a document properties header.
let properties = UIDocumentProperties(url: document.fileURL) if let itemProvider = NSItemProvider(contentsOf: document.fileURL) { properties.dragItemsProvider = { _ in [UIDragItem(itemProvider: itemProvider)] } properties.activityViewControllerProvider = { UIActivityViewController(activityItems: [itemProvider], applicationActivities: nil) } } navigationItem.documentProperties = properties
-
6:36 - Adopt rename title menu action and rename UI
override func viewDidLoad() { navigationItem.renameDelegate = self } func navigationItem(_ navigationItem: UINavigationItem, didEndRenamingWith title: String) { // Rename document using methods appropriate to the app’s data model }
-
7:09 - Adopt system provided title menu actions.
override func duplicate(_ sender: Any?) { // Duplicate document } override func move(_ sender: Any?) { // Move document } func didOpenDocument() { ... navigationItem.titleMenuProvider = { [unowned self] suggested in var children = suggested ... return UIMenu(children: children) } }
-
7:10 - Add custom title menu actions
func didOpenDocument() { ... navigationItem.titleMenuProvider = { [unowned self] suggested in var children = suggested children += [ UIMenu(title: "Export…", image: UIImage(systemName: "arrow.up.forward.square"), children: [ UIAction(title: "HTML", image: UIImage(systemName: "safari")) { ... }, UIAction(title: "PDF", image: UIImage(systemName: "doc")) { ... } ]) ] return UIMenu(children: children) } }
-
9:35 - Enable customization for center items
navigationItem.customizationIdentifier = "editorView"
-
10:00 - Define a fixed center item group.
UIBarButtonItem(title: "Sync Scrolling", ...).creatingFixedGroup()
-
10:23 - Define an optional (customizable) center item group.
UIBarButtonItem(title: "Add Link", ...).creatingOptionalGroup(customizationIdentifier: "addLink")
-
10:56 - Define a non-default optional center item group.
UIBarButtonItemGroup.optionalGroup(customizationIdentifier: "textFormat", isInDefaultCustomization: false, representativeItem: UIBarButtonItem(title: "Format", ...) items: [ UIBarButtonItem(title: "Bold", ...), UIBarButtonItem(title: "Italics", ...), UIBarButtonItem(title: "Underline", ...), ])
-
13:16 - Define a custom menu representation for a bar button item group.
sliderGroup.menuRepresentation = UIMenu(title: "Text Size", preferredElementSize: .small, children: [ UIAction(title: "Decrease", image: UIImage(systemName: "minus.magnifyingglass"), attributes: .keepsMenuPresented) { ... }, UIAction(title: "Reset", image: UIImage(systemName: "1.magnifyingglass"), attributes: .keepsMenuPresented) { ... }, UIAction(title: "Increase", image: UIImage(systemName: "plus.magnifyingglass"), attributes: .keepsMenuPresented) { ... }, ])
-
15:10 - Enable multiple selection and keyboard focus in a UICollectionView.
// Enable multiple selection collectionView.allowsMultipleSelection = true // Enable keyboard focus collectionView.allowsFocus = true // Allow keyboard focus to drive selection collectionView.selectionFollowsFocus = true
-
16:11 - Add a primary action to UICollectionView items.
func collectionView(_ collectionView: UICollectionView, performPrimaryActionForItemAt indexPath: IndexPath) { // Scroll to the tapped element if let element = dataSource.itemIdentifier(for: indexPath) { delegate?.outline(self, didChoose: element) } }
-
16:56 - Add a multi-item menu to UICollectionView.
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? { if indexPaths.count == 0 { // Construct an empty space menu } else if indexPaths.count == 1 { // Construct a single item menu } else { // Construct a multi-item menu } }
-
18:12 - Enable Find and Replace in UITextView.
textView.isFindInteractionEnabled = true
-
18:34 - Add custom actions to UITextView's edit menu.
func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? { if textView.selectedRange.length > 0 { let customActions = [ UIAction(title: "Hide", ... ) { ... } ] return UIMenu(children: customActions + suggestedActions) } return nil }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.