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.

4.8 (19) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.

A window with information of a gem.

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.

A list of gems on iPhone. Each row shows a thumbnail, plus the name and main color of the gem.

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.

A window with a list of gems.

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:

  1. iOS 14 introduced a new view modifier: toolbar(content:). This modifier takes a ToolbarItem that represents the contents of the toolbar.
  2. Add a ToolbarItem with a single button to toggle favorite on the gem.
  3. 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.

A view with information about a gem. At the top, a filled heart button.

Now, build and run on macOS. Favorite a gem to see the result.

A window with a list of gems and a view with information of a gem. At the top, a filed red heart button.

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.

A small window with a view of informations of a gem. At the top, a menu with the button to unfavorite.

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:

  1. First, create a TabView as the root view.
  2. Next, add GemList as its first view, with a Label with the title “All” and the image of a list bullet.
  3. Add FavoriteGems as the second view, with a Label 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.

A list of favorite gems on iPhone. Each row has a thumbnail, name and main color name.

Next, change the target to macOS. Build and run to see how SwiftUI adapts the UI on macOS.

A window with a list of favorite gems and an empty content view. At the top are two buttons, one for all and one for favorite gems.

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.

An illustration of a common macOS UI layout. A panel for the sidebar, a list, and a content view.

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.

A list of targets of the project. macOS target is selected.

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.