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 Fabrizio Brancati.

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

Creating a Navigation Link

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

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

struct DetailView: View {
  let discipline: String
  var body: some View {

This has a single property and, like any Swift struct, it has a default initializer — in this case, DetailView(discipline: String). The view is 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, and add the .navigationDestination(for:destination:) destination modifier:

List(disciplines, id: \.self) { discipline in
  NavigationLink(value: discipline) {
.navigationDestination(for: String.self, destination: { discipline in
  DetailView(discipline: discipline)

There’s no prepare(for:sender:) rigmarole — you 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:

NavigationLink disclosure arrow on each row

Tap a row to show its detail view:

NavigationLink to DetailView

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

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

Add a title to the DetailView:

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

This view is presented by a NavigationLink, so it doesn’t need its own NavigationStack 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:

Inline navigation bar title in DetailView

Now you know how to create a basic master-detail app. You used String objects, to avoid clutter that might obscure how lists and navigation work. But list items are usually instances of a model type 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 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 Artwork.swift file add the following:

extension Artwork: Hashable {
  static func == (lhs: Artwork, rhs: Artwork) -> Bool {
    lhs.id == rhs.id
  func hash(into hasher: inout Hasher) {

This will let you use Artwork inside a List, because all items must be Hashable.

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.

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 useful later!

Conforming to Identifiable

But there’s an even better way: Go back to Artwork.swift, and add this extension, outside the Artwork struct:

extension Artwork: Identifiable { }

The id property is all you need to make Artwork conform to Identifiable, and you’ve already added that.

Now you can avoid specifying id parameter entirely:

List(artworks) { artwork in

Looks much neater now! Because Artwork conforms to Identifiable, List knows it has an id property and automatically uses this property for its id argument.

Then, 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) { artwork in
  NavigationLink(value: artwork) {
.navigationDestination(for: Artwork.self, destination: { artwork in
  DetailView(artwork: artwork)

Also, edit DetailView to use Artwork:

struct DetailView: View {
  let artwork: Artwork
  var body: some View {
    .navigationBarTitle(Text(artwork.title), displayMode: .inline)

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

Showing More Detail

Artwork objects have lots of information you can display, so update your DetailView to show more details.

First, create a new SwiftUI View file: Command-N ▸ iOS ▸ User Interface ▸ SwiftUI View. Name it DetailView.swift.

Replace import Foundation with import SwiftUI.

Delete DetailView completely from ContentView.swift. You’ll replace it with a whole new view.

Add the following to DetailView.swift:

struct DetailView: View {
  let artwork: Artwork
  var body: some View {
    VStack {
        .frame(maxWidth: 300, maxHeight: 600)
        .aspectRatio(contentMode: .fit)
      Text("\(artwork.reaction) \(artwork.title)")
      Text("Artist: \(artwork.artist)")
    .navigationBarTitle(Text(artwork.title), displayMode: .inline)

You’re displaying several views in a vertical layout, so everything is in a VStack.

First is the Image: The artData images are all different sizes and aspect ratios, so you specify aspect-fit, and constrain the frame to at most 300 points wide by 600 points high. However, these modifiers won’t take effect unless you first modify the Image to be resizable.

You modify the Text views to specify font size and multilineTextAlignment, because some of the titles and descriptions are too long for a single line.

Finally, you add some padding around the stack.

You also need a preview, so add it:

struct DetailView_Previews: PreviewProvider {
  static var previews: some View {
    DetailView(artwork: artData[0])

Refresh the preview:

Artwork detail view

There’s Prince Jonah! In case you’re curious, Kalanianaole has seven syllables, four of them in the last six letters ;].

The navigation bar doesn’t appear when you preview or even live-preview DetailView, because it doesn’t know it’s in a navigation stack.

Go back to ContentView.swift and tap a row to see the complete detail view:

Artwork detail view with navigation bar title