SwiftUI Tutorial: Navigation

In this tutorial, you’ll use SwiftUI to implement the navigation of a master-detail app. You’ll learn how to implement a navigation stack, a navigation bar button, a context menu and a modal sheet. By Audrey Tam.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 6 of this article. Click here to view the first page.

Navigating to the Detail View

You’ve just seen how easy it is to display the master view. It’s just about as easy to navigate to the detail view.

First, embed List in a NavigationView, like this:

NavigationView {
  List(disciplines, id: \.self) { discipline in
    Text(discipline)
  }
  .navigationBarTitle("Disciplines")
}

This is like embedding a view controller in a navigation controller: You can now access all the navigation things, like the navigation bar title. Notice .navigationBarTitle modifies List, not NavigationView. You can declare more than one view in a NavigationView, and each can have its own .navigationBarTitle.

Refresh the preview to see how this looks:

Nice! You get a large title by default. That’s fine for the master list, but you’ll do something different for the detail view’s title.

Creating a Navigation Link

NavigationView also enables NavigationLink, which needs a destination view and a label — like creating a segue in a storyboard, but without those pesky segue identifiers.

So first, create your DetailView. For now, just declare it in ContentView.swift, below the ContentView struct:

struct DetailView: View {
  let discipline: String
  var body: some View {
    Text(discipline)
  }
}

This has a single property and, like any Swift struct, it has a default initializer — in this case, DetailView(discipline: String). The view is just the String itself, presented in a Text view.

Now, inside the List closure in ContentView, make the row view Text(discipline) into a NavigationLink button:

List(disciplines, id: \.self) { discipline in
  NavigationLink(
    destination: DetailView(discipline: discipline)) {
      Text(discipline)
  }
}

There’s no prepare(for:sender:) rigmarole — you simply pass the current list item to DetailView to initialize its discipline property.

Refresh the preview to see a disclosure arrow at the trailing edge of each row:

Start Live Preview, then tap a row to show its detail view:

And zap, it just works! Notice you get the usual back button, too.

But the view looks so plain — it doesn’t even have a title.

So add a title, like this:

var body: some View {
  Text(discipline)
    .navigationBarTitle(Text(discipline), displayMode: .inline)
}

This view is presented by a NavigationLink, so it doesn’t need its own NavigationView to display a navigationBarTitle. But this version of navigationBarTitle requires a Text view for its title parameter — you’ll get peculiarly meaningless error messages if you try it with just the discipline string. Option-click the two navigationBarTitle modifiers to see the difference in the title and titleKey parameter types.

The displayMode: .inline argument displays a normal size title.

Start Live-preview again, and tap a row to see the title:

So now you know how to create a basic master-detail app. You used String objects, to avoid any clutter that might obscure how lists and navigation work. But list items are usually instances of a model type that you define. It’s time to use some real data.

Revisiting Honolulu Public Artworks

The starter project contains the Artwork.swift file. Artwork is a struct with eight properties, all constants except for the last, which the user can set:

struct Artwork {
  let artist: String
  let description: String
  let locationName: String
  let discipline: String
  let title: String
  let imageName: String
  let coordinate: CLLocationCoordinate2D
  var reaction: String
}

Below the struct is artData, an array of Artwork objects. It’s a subset of the data used in our MapKit Tutorial: Getting Started — public artworks in Honolulu.

The reaction property of some of the artData items is 💕, 🙏 or 🌟 but, for most items, it’s just an empty String. The idea is when users visit an artwork, they set a reaction to it in the app. So an empty-string reaction means the user hasn’t visited this artwork yet.

Now start updating your project to use Artwork and artData: In ContentView, add this property:

let artworks = artData

Delete the disciplines array.

Then replace disciplines, discipline and ‘Disciplines’ with artworks, artwork and “Artworks”:

List(artworks, id: \.self) { artwork in
  NavigationLink(
    destination: DetailView(artwork: artwork)) {
      Text(artwork.title)
  }
}
.navigationBarTitle("Artworks")

And also edit DetailView to use Artwork:

struct DetailView: View {
  let artwork: Artwork

  var body: some View {
    Text(artwork.title)
      .navigationBarTitle(Text(artwork.title), displayMode: .inline)
  }
}

Ah, Artwork isn’t Hashable! So change \.self to \.title:

List(artworks, id: \.title) { artwork in

You’ll soon create a separate file for DetailView, but this will do for now.

Now, take another look at that id parameter in the List view.

Creating Unique id Values With UUID()

The argument of the id parameter can use any combination of the list item’s Hashable properties. But, like choosing a primary key for a database, it’s easy to get it wrong, then find out the hard way that your identifier isn’t as unique as you thought.

The Artwork title is unique but, to see what happens if your id values aren’t unique, replace \.title with \.discipline in List:

List(artworks, id: \.discipline) { artwork in

Refresh the preview (Option-Command-P):

The titles in artData are all different, but the list thinks all the statues are “Prince Jonah Kuhio Kalanianaole”, all the murals are “The Makahiki Festival Mauka Mural”, and all the plaques are “Amelia Earhart Memorial Plaque”. Each of these is the first item of that discipline that appears in artData. And this is what can happen if your list items don’t have unique id values.

Fortunately, the solution is easy — it’s pretty much what many databases do: Add an id property to your model type, and use UUID() to generate a unique identifier for every new object.

In Artwork.swift, add this property at the top of the Artwork property list:

let id = UUID()

You use UUID() to let the system generate a unique ID value, because you don’t care about the actual value of id. This unique ID will be very useful later!

Then, in ContentView.swift, change the id argument in List to \.id:

List(artworks, id: \.id) { artwork in

Refresh the preview:

Now each artwork has a unique id value, so the list displays everything properly.

Note: If refreshing the preview alone doesn’t fix the list, build your project (Command-B) and then refresh the preview.