Multiplatform App Tutorial: SwiftUI and Xcode 12
Learn how to use Xcode 12’s multiplatform app template and SwiftUI to write a single app that runs on every Apple platform. By Renan Benatti Dias.
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
Multiplatform App Tutorial: SwiftUI and Xcode 12
25 mins
- Getting Started
- Considering the Project Structure
- Understanding the New App and Scene Protocol
- Running on macOS
- Understanding how SwiftUI Adapts to the Platform
- Polishing the macOS app
- Adding a minimum width to your list
- Adding a navigation title
- Working With Toolbars
- Understanding tab views on different platforms
- Optimizing the User Experience for Each Platform
- Updating the macOS UI
- Updating the iOS UI
- Understanding Independent Scenes and View States
- Adding Extra Functionality for macOS
- Creating the Preferences View
- Adding a Keyboard Shortcut
- Where to Go From Here?
Adding a minimum width to your list
Right now, you can resize the side list of gems on macOS and shrink it down to nothing.
This is a behavior that might confuse some users. SwiftUI gives you modifiers to handle this kind of situation without writing specific code for each platform.
Open GemList.swift, find // TODO: Add min frame here.
and add this line below the comment:
.frame(minWidth: 250)
Build and run on an iPhone simulator to see the result.
This modifier adds a minimum width to the list. This might not make much sense on iOS, since a List
will use the entire width of the view. However, on macOS, this modifier ensures the List
keeps its width to a minimum of 250 points, while still allowing the user to resize it.
Change the target to RayGem (macOS) and build and run again. Try to resize the list.
Notice how the side list can still be resized, but it always stays wider than 250 points.
Adding a navigation title
Still in GemList.swift, notice the modifier at the bottom of the List
, navigationTitle(_:)
. This is a new modifier, introduced on iOS 14, to configure the view’s title. On iOS and watchOS, it will use the string as the title of the navigation view. iPadOS will set the primary navigation view title and the title in the App Switcher. This is important to differentiate instances of your app. On macOS, the window title bar and Mission Control use this string as the title.
Working With Toolbars
Now, it’s time to give users the power to save their favorite gems.
Inside DetailsView.swift, add the following to the bottom of the view:
func toggleFavorite() {
gem.favorite.toggle()
try? viewContext.save()
}
This method toggles the favorite
property on the current gem
and saves the change to Core Data.
Next, find // TODO: Add favorite button here
and add the following code below the comment:
// 1
.toolbar {
// 2
ToolbarItem {
Button(action: toggleFavorite) {
// 3
Label(
gem.favorite ? "Unfavorite" : "Favorite",
systemImage: gem.favorite ? "heart.fill" : "heart"
)
.foregroundColor(.pink)
}
}
}
Here’s a breakdown of the code:
- iOS 14 introduced a new view modifier:
toolbar(content:)
. This modifier takes aToolbarItem
that represents the contents of the toolbar. - Add a
ToolbarItem
with a single button to togglefavorite
on thegem
. - Next, add a
Label
as the content of the button, with the title being “Favorite” or “Unfavorite” and the image of a heart.
Build and run. Then, favorite a gem.
Now, build and run on macOS. Favorite a gem to see the result.
SwiftUI takes the ToolbarItem
and places it in the expected position of each platform. On iOS, it uses the image of the Label
as the button on the navigation bar, following the color scheme of the bar. On macOS, it also uses the image of the Label
. However, if you resize the window and leave no space for the buttons on the toolbar, it creates a menu button with the title of the Label
.
Resize the window to the minimum width possible to see this.
SwiftUI adapts the UI for each platform, finding the best way to display the button, even when you resize the window.
Understanding tab views on different platforms
Now that users can favorite their gems, it would be nice to have a way to list these favorites.
The starter project already comes with the code for this, the FavoriteGems
view. This view fetches and lists all the gems with the favorite
property set to true
.
Open ContentView.swift and add the following enum to the top of the file:
enum NavigationItem {
case all
case favorites
}
This enum describes the two tabs of your app. Next, add a tab view by replacing the contents of body
with the following:
// 1
TabView {
// 2
NavigationView {
GemList()
}
.tabItem { Label("All", systemImage: "list.bullet") }
.tag(NavigationItem.all)
// 3
NavigationView {
FavoriteGems()
}
.tabItem { Label("Favorites", systemImage: "heart.fill") }
.tag(NavigationItem.favorites)
}
Here’s what the code above does:
- First, create a
TabView
as the root view. - Next, add
GemList
as its first view, with aLabel
with the title “All” and the image of a list bullet. - Add
FavoriteGems
as the second view, with aLabel
with the title Favorites and the image of a heart.
Build and run on iOS. Favorite some gems and open the Favorites tab to see them listed there.
Next, change the target to macOS. Build and run to see how SwiftUI adapts the UI on macOS.
Fantastic! You already have a simple app that runs on iOS and macOS! Take a moment to enjoy what you’ve accomplished so far. :]
Optimizing the User Experience for Each Platform
SwiftUI tries to adapt the UI declared in code to each platform. A TabBar
on iOS has its bar at the bottom and an image and text as buttons. On macOS, it uses a bar on the top of the view with titles, a lot like a segmented view.
Even though SwiftUI handles adapting the UI on each platform, that doesn’t mean it always creates what a user expects. Instead of using a TabBar
on macOS, a better layout would be a Sidebar with a list of categories. Then, a list would display each element of the selected category.
Your app already works on both platforms, but users expect an optimal experience everywhere. Thankfully, Apple added a way to create platform-specific views in multiplatform apps. This is exactly what the macOS and iOS groups in Xcode are for! You’ll update the tab bar in your app to use a sidebar layout for macOS now.
Updating the macOS UI
Create a new SwiftUI View file inside the macOS group and name it GemListViewer.swift. Select the macOS target membership only.
First, add a new property and method to the view:
@State var selection: NavigationItem? = .all
func toggleSideBar() {
NSApp.keyWindow?.firstResponder?.tryToPerform(
#selector(NSSplitViewController.toggleSidebar),
with: nil)
}
This is a state variable that you’ll update with the currently selected category in the sidebar: all gems or only the favorite ones. toggleSideBar()
will show or hide the sidebar when the user clicks a button; you’ll hook that up in a bit.
Next, add the following computed property to the view:
var sideBar: some View {
List(selection: $selection) {
NavigationLink(
destination: GemList(),
tag: NavigationItem.all,
selection: $selection
) {
Label("All", systemImage: "list.bullet")
}
.tag(NavigationItem.all)
NavigationLink(
destination: FavoriteGems(),
tag: NavigationItem.favorites,
selection: $selection
) {
Label("Favorites", systemImage: "heart")
}
.tag(NavigationItem.favorites)
}
// 3
.frame(minWidth: 200)
.listStyle(SidebarListStyle())
.toolbar {
// 4
ToolbarItem {
Button(action: toggleSideBar) {
Label("Toggle Sidebar", systemImage: "sidebar.left")
}
}
}
}
You create a sidebar view that contains a List
with two NavigationLinks
— one for GemList
and one for FavoriteGems
. By using SidebarListStyle
you tell SwiftUI to display this List
as a sidebar for users to select which category they want to see. You also create a ToolbarItem
inside the toolbar with a button to toggle the sidebar. It’s expected behavior in macOS apps to have the ability to hide and show the sidebar.
Next, replace the contents of body
with the following:
NavigationView {
sideBar
Text("Select a category")
.foregroundColor(.secondary)
Text("Select a gem")
.foregroundColor(.secondary)
}
This shows your sidebar together with some text.
Finally, replace the content of previews
with the following:
GemListViewer()
.environment(
\.managedObjectContext,
PersistenceController.preview.container.viewContext)
You’re done with your macOS UI, but you can’t see it just yet. First, you’ll move on to the iOS UI.