Streaming is available in most browsers,
and in the Developer app.
-
SwiftUI on iPad: Add toolbars, titles, and more
Get ready to tune up your iPad app's toolbars with SwiftUI. We'll show you how you can structure toolbars to take advantage of the space available on iPad and help people maximize their productivity. We'll also take you through customization, explore the latest ways you can represent documents, and more. This is the second session in a two-part series. To get the most out of this video, we recommend starting with “SwiftUI on iPad: Organize your interface.”
Resources
Related Videos
WWDC23
WWDC22
-
Download
♪ ♪ Harry: Hi, I'm Harry, an engineer on the SwiftUI team. Welcome to the second part of the SwiftUI on iPad series. In the first part of this series, my colleague Raj dove deep into how you can use lists, tables, selection, and split views to really make your apps shine using the iPad's larger screens and various input devices. If you haven't seen that session, I highly recommend starting with that. I really enjoyed seeing Raj build out the Places app and I wanted to add some features of my own. So in this session I'll be looking at something near and dear to my heart, toolbars! In SwiftUI, the toolbar API configures many system bars like the navigation bar or bottom bar on iOS or the window toolbar on macOS. Toolbars provide quick actions to a lot of your most common features. Creating a good toolbar can really improve the productivity of people using your app.
I've been spending a lot of time thinking about toolbars and how Places could use some of the new toolbar features in iOS 16. I'd like to start by briefly showing you what I've built to give you a taste of what is now possible on iPad.
After all my changes, here is the updated Places app.
You might notice some new features, like leading aligned navigation titles, title menus, title menu headers, and centered aligned toolbar items. If you've used a Mac, you might also be familiar with features like Toolbar customization, which allows people to make toolbars uniquely their own. These powerful Mac features are making their debut on the iPad.
To start this session, I'll show some enhancements to the toolbar API. Then I'll walk you through some new APIs for titles and documents. So let's dive right in with toolbars. Now, many of you have already configured toolbars in your own iOS app and to optimize for a smaller screen, you might've added a menu like I did when I first looked at Places.
My menu looked like this in code.
You can see I have a toolbar item with a primary action placement. Inside, I have a more menu, with a few controls as the menu's content. Let's see what this looks like on iPad.
As you might have guessed, it doesn't really take advantage of the space available there. What's great, though, is that in iOS 16, toolbars can implement these kind of menus on your behalf. We call these overflow menus and to make best use of them, I'll want to restructure the content of my toolbar.
I'll start by converting the toolbar item into a toolbar item group. I'll then remove the menu and make its content the content of the toolbar item group. This group inserts individual toolbar items for each view in the group. On iPad and Mac, this is all that's needed to automatically move items into an overflow menu when needed. There's more I can do here, but before I can get to that, I'll need to think about the placement of my toolbar items.
Placements define the area in which a toolbar item is rendered. Different placements can resolve to the same area. You can think of a navigation bar as having three distinct areas: the leading, the center, and the trailing area. The leading and trailing areas typically contain controls. While the center contains your app's navigation title. Let's look at this in the context of Places.
In Places my primary action toolbar item group resolves to the trailing area on iPad or Mac. Primary actions represent the most common action available to the user for a particular screen.
In iOS 16, there's a new placement called secondary action. These items represent controls that are not the most used controls, but still warrant their own toolbar items. Actions like favoriting and editing aren't the most important in Places, so I'll make them secondary actions.
By default, secondary actions will not be visible in the toolbar. Instead, they live in the overflow menu. You can change that behavior by using the new toolbarRole modifier.
This modifier influences the behavior of a toolbar by assigning it a semantic role. Here I pass an editor role which tells the navigation bar that it should be optimized for editing content. The navigation bar interprets this as a desire to have more space to render toolbar items and so it moves the navigation title from the center area to the leading area. This makes room for secondary actions to be placed in the center before moving to the overflow menu. In compact size classes, the navigation bar doesn't change and continues to render the title in the center.
Using the secondary action and toolbar role API really lets Places start to take advantage of the iPad's size. With the center available to me, I can add more items to my toolbar, like a noise button, or a comfort level button, or a trash button. But if I start adding a lot of items, my toolbar might become unwieldy for some users. MacOS has long supported customizable toolbars which lets each person choose the toolbar best for them and I'm excited to say that now iPadOS also supports user customization. You can use the existing toolbar customization API that's worked on macOS to adopt this feature. Let's look at that now.
Only toolbar items are customizable so I'll first split up my toolbar item group into individual toolbar items. Note that there is no functional difference after this change. Customization also requires every item in the toolbar to be associated with a unique identifier so I'll add IDs to each of my items. It's important for these IDs to be unique and consistent across app launches. When a user customizes their toolbar, SwiftUI persists these IDs and uses them to look up the associated view to render. Finally, I'll add an ID to the toolbar modifier as a whole. Altogether, this opts the toolbar into supporting customization.
Unique to customizable toolbars, toolbar items have the ability to not be initially present in the toolbar. These items start their life in the customization popover and can be added to the toolbar later. Because these items aren't initially present, this is a good option for actions that are more useful to specific workflows. Let's check them out.
I want to hide some of my current toolbar items to make my new items easier to see.
Now let's add a toolbar item that contains a share link. Share links are new in SwiftUI and rely on a new protocol called transferable. For more information on Transferable and ShareLink, be sure to check out the Meet Transferable session. With my toolbar item in place, I'll provide a value of false to the shows by default parameter which makes this item not be initially present in the bar.
Now you can see my share link living its best life in the customization popover. And I'm able to drag it from the customization popover into the bar. People are going to love that. With my share link in place, I started to think about the relationship between my toolbar items. After moving the share link into the bar, I noticed that my image and map adjustment items are separated, but I think of these items as a group of quick editing control, and I'd like to model that relationship in the toolbar itself.
iOS 16 and macOS Ventura support modeling this relationship by using ControlGroup. I'll show you how. You can see I have two individual toolbar items for my image and map adjustment actions. To group them together, I'll first move them to the same item. Then I'll wrap them in a control group.
A user can now add or remove them from the toolbar as a unit. That's pretty cool, but I can take this even further using a new API available on ControlGroup. By providing a label to the control group, this group of items can collapse into its own smaller menu before moving into the overflow menu.
The toolbar is really starting to come together. There's one last change I'd like to make. Adding a new place is a common and important action, so I'd like to add a toolbar item for that.
To do that, I'll add a new button to my toolbar, but this time I'll use the primary action placement as I consider the new button to be the most common action.
This placement highlights an important distinction between iOS and macOS. All items within a customizable toolbar modifier support customization on macOS, but on iPadOS, only secondary actions do. So my new button here renders in the trailing area of the bar and is not customizable by the user. Wow! That was a lot about toolbars and those aren't the only improvements. Navigation titles gained some new features as well around menus, documents, and more. Let's take documents as an example. There are lot of kinds of documents. You might be familiar with documents that are managed by the document group API. Document groups come with a lot of built-in functionality for representing and managing their documents. All of what I'm about to talk about comes for free when using document groups.
In Places, though, an individual place can be considered a document even though Places isn't using a document group. It has properties that you can edit, you can add or remove them to the Places app, and you can share a place with your friends. Let's look at how we can show off this relationship in a non-document group based app. I already associate the name of the place as the navigation title of the view. So I'm already associating a piece of the place to the title of my toolbar. In iOS 16, I can take that even further by using new navigation title modifiers. Navigation titles now support presenting a menu. You can kind of think of this like the File menu on macOS. To create one of these menus, provide a set of actions to the navigation title, just like you would a normal menu. Notice that the title now has a menu indicator attached to it that presents a menu populated with your actions. And that's not all navigation titles can do. One of my favorite parts is its new ability to support editable titles. You can pass a binding to your navigation title and this tells the toolbar that you support editing the title. All you're missing is a way to actually start editing. You can use the new RenameButton inside your titles menus actions to accomplish this.
Tapping on the rename button allows you to start renaming the title. Just like you can associate a navigation title to your view, you can now also associate a document, like my place. When provided a document, the title menu renders a specialized header that shows a preview of that document. The preview can be dragged and there's quick access to share the document. To get one of these headers, associate your navigation document to your view using the new navigation document modifier. You can do this by providing a type that conforms to transferable, or by providing a URL directly. Here I'll provide a URL that will open the Maps app to the place I'm viewing. The navigation document modifier will also configure the proxy icon of the window toolbar on macOS when providing a URL.
After all that, I think I'll take a break from updating my app's toolbars. Can you believe all the features I've added in this time? I can't wait to start using them. I covered a lot in my journey to improve the Places experience on iPad. Toolbars on iPad have gained a lot of new features like overflow menus and user customization. Use the secondary action placement and customization APIs to really make the most of the larger space available on iPad and Mac.
Titles have gained some new ways to represent themselves in the toolbar. Use a navigation title or create a title menu or support title renaming. And remember to use the navigation document modifier when appropriate. I hope you've enjoyed the SwiftUI on iPad series. With all the improvements in tables, selection, toolbars, and more, go out and take your iPad apps to the next level. Thank you, and have a great WW.
-
-
0:01 - Explicit More Menu
PlaceDetailContent(place: $place) .toolbar { ToolbarItem(placement: .primaryAction) { Menu { FavoriteToggle(place: $place) AdjustImageButton(place: $place) AdjustMapButton(place: $place) } label: { Label( "More", systemImage: "ellipsis.circle") } } }
-
0:02 - Menu in ToolbarItemGroup
PlaceDetailContent(place: $place) .toolbar { ToolbarItemGroup(placement: .primaryAction) { Menu { FavoriteToggle(place: $place) AdjustImageButton(place: $place) AdjustMapButton(place: $place) } label: { Label("More", systemImage: "ellipsis.circle") } } }
-
0:03 - ToolbarItemGroup with Menu Content
PlaceDetailContent(place: $place) .toolbar { ToolbarItemGroup(placement: .primaryAction) { FavoriteToggle(place: $place) AdjustImageButton(place: $place) AdjustMapButton(place: $place) } }
-
0:04 - Secondary Action ToolbarItemGroup
PlaceDetailContent(place: $place) .toolbar { ToolbarItemGroup(placement: .secondaryAction) { FavoriteToggle(place: $place) AdjustImageButton(place: $place) AdjustMapButton(place: $place) } }
-
0:05 - Toolbar Role
PlaceDetailContent(place: $place) .toolbar { ToolbarItemGroup(placement: .secondaryAction) { FavoriteToggle(place: $place) AdjustImageButton(place: $place) AdjustMapButton(place: $place) } } .toolbarRole(.editor)
-
0:06 - Individual ToolbarItems
PlaceDetailContent(place: $place) .toolbar { ToolbarItem(placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(placement: .secondaryAction) { AdjustImageButton(place: $place) } ToolbarItem(placement: .secondaryAction) { AdjustMapButton(place: $place) } } .toolbarRole(.editor)
-
0:07 - Customizable ToolbarItems
PlaceDetailContent(place: $place) .toolbar(id: "place") { ToolbarItem(id: "favorite", placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(id: "image", placement: .secondaryAction) { AdjustImageButton(place: $place) } ToolbarItem(id: "map", placement: .secondaryAction) { AdjustMapButton(place: $place) } } .toolbarRole(.editor)
-
0:08 - ShareLink ToolbarItem
PlaceDetailContent(place: $place) .toolbar(id: "place") { ToolbarItem(id: "favorite", placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(id: "image", placement: .secondaryAction) { AdjustImageButton(place: $place) } ToolbarItem(id: "map", placement: .secondaryAction) { AdjustMapButton(place: $place) } ToolbarItem(id: "share", placement: .secondaryAction) { ShareLink(item: place) } } .toolbarRole(.editor)
-
0:09 - Non-default ShareLink ToolbarItem
PlaceDetailContent(place: $place) .toolbar(id: "place") { ToolbarItem(id: "favorite", placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(id: "image", placement: .secondaryAction) { AdjustImageButton(place: $place) } ToolbarItem(id: "map", placement: .secondaryAction) { AdjustMapButton(place: $place) } ToolbarItem(id: "share", placement: .secondaryAction, showsByDefault: false) { ShareLink(item: place) } } .toolbarRole(.editor)
-
0:10 - ControlGroup in ToolbarItem
PlaceDetailContent(place: $place) .toolbar(id: "place") { ToolbarItem(id: "favorite", placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(id: "image", placement: .secondaryAction) { ControlGroup { AdjustImageButton(place: $place) AdjustMapButton(place: $place) } } ToolbarItem(id: "share", placement: .secondaryAction, showsByDefault: false) { ShareLink(item: place) } } .toolbarRole(.editor)
-
0:11 - ControlGroup in ToolbarItem with Label
PlaceDetailContent(place: $place) .toolbar(id: "place") { ToolbarItem(id: "favorite", placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(id: "image", placement: .secondaryAction) { ControlGroup { AdjustImageButton(place: $place) AdjustMapButton(place: $place) } label: { Label("Edits", systemImage: "wand.and.stars") } } } .toolbarRole(.editor)
-
0:12 - NewButton ToolbarItem
PlaceDetailContent(place: $place) .toolbar(id: "place") { ToolbarItem(id: "new", placement: .primaryAction) { NewButton() } ToolbarItem(id: "favorite", placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(id: "image", placement: .secondaryAction) { ControlGroup { AdjustImageButton(place: $place) AdjustMapButton(place: $place) } label: { Label("Edits", systemImage: "wand.and.stars") } } ToolbarItem(id: "share", placement: .secondaryAction, showsByDefault: false) { ShareLink(item: place) } } .toolbarRole(.editor)
-
0:13 - Navigation Title
PlaceDetailContent(place: $place) // toolbar customizations ... .navigationTitle(place.name)
-
0:14 - Navigation Title with Menu
PlaceDetailContent(place: $place) // toolbar customizations ... .navigationTitle(place.name) { MyPrintButton() }
-
0:15 - Editable Navigation Title with Menu
PlaceDetailContent(place: $place) // toolbar customizations ... .navigationTitle($place.name) { MyPrintButton() }
-
0:16 - Editable Navigation Title with RenameButton
PlaceDetailContent(place: $place) // toolbar customizations ... .navigationTitle($place.name) { MyPrintButton() RenameButton() }
-
0:17 - Navigation Document
PlaceDetailContent(place: $place) // toolbar customizations ... .navigationTitle($place.name) { MyPrintButton() RenameButton() } .navigationDocument(place.url)
-
-
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.