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 4 of 6 of this article. Click here to view the first page.

Reacting to Artwork

One feature that’s missing from this app is a way for users to set a reaction to an artwork. In this section, you’ll add a context menu to the list row to let users set their reaction for that artwork.

Adding a Context Menu

Still in ContentView.swift, make artworks a @State variable:

@State var artworks = artData

The ContentView struct is immutable, so you need this @State property wrapper to be able to assign a value to an Artwork property.

Next, add this helper method stub to ContentView:

private func setReaction(_ reaction: String, for item: Artwork) { }

Then add the contextMenu modifier to the list row Text view:

Text("\(artwork.reaction)  \(artwork.title)")
  .contextMenu {
    Button("Love it: 💕") {
      self.setReaction("💕", for: artwork)
    }
    Button("Thoughtful: 🙏") {
      self.setReaction("🙏", for: artwork)
    }
    Button("Wow!: 🌟") {
      self.setReaction("🌟", for: artwork)
    }
}
Note: Whenever you use a view property or method inside a closure, you must use self. — don’t worry, if you forget, Xcode will tell you and offer to fix it ;].

The context menu shows three buttons, one for each reaction. Each button calls setReaction(_:for:) with the appropriate emoji.

Finally, implement the setReaction(_:for:) helper method:

private func setReaction(_ reaction: String, for item: Artwork) {
  if let index = self.artworks.firstIndex(
    where: { $0.id == item.id }) {
    artworks[index].reaction = reaction
  }
}

And here’s where the unique ID values do their stuff! You compare id values to find the index of this item in the artworks array, then set that array item’s reaction value.

Note: You might be thinking it would be easier to just set artwork.reaction = "💕" directly. Unfortunately, the artwork list iterator is a let constant.

Refresh the live preview (Option-Command-P), then touch and hold an item to display the context menu. Tap a context menu button to select a reaction, or tap outside the menu to close it.

How does that make you feel? 💕 🙏 🌟!

Creating a Tab View App

Now you’re ready to construct an alternative app that uses a tab view to list either all artworks or just the unvisited artworks.

Start by creating a new SwiftUI View file to create your alternative master view. Name it ArtTabView.swift.

Next, copy all the code inside ContentViewnot the struct ContentView line or the closing brace — and paste it inside the struct ArtTabView closure, replacing the boilerplate body code.

Now, with the canvas open (Option-Command-Return), Command-click List, and select Extract Subview from the menu:

Name the new subview ArtList.

Next, delete the navigationBarItems toggle. The second tab will replace this feature.

Now add these properties to ArtList:

@Binding var artworks: [Artwork]
let tabTitle: String
let hideVisited: Bool

You’ll pass a binding to the @State variable artworks, from ArtTabView to ArtList. This is so the context menu will still work.

Each tab will need a navigation bar title. And you’ll use hideVisited to control which items appear, although this no longer needs to be a @State variable.

Next, move showArt and setReaction from ArtTabView into ArtList, to handle these jobs in ArtList.

Then replace .navigationBarTitle("Artworks") with:

.navigationBarTitle(tabTitle)

Almost there: In the body of ArtTabView, add the necessary parameters to ArtList:

ArtList(artworks: $artworks, tabTitle: "All Artworks", hideVisited: false)

Refresh the preview to check that this all still works:

Looks good! Now, a TabView with two tabs by replacing the body definition for ArtTabView with:

TabView {
  NavigationView {
    ArtList(artworks: $artworks, tabTitle: "All Artworks", hideVisited: false)
    DetailView(artwork: artworks[0])
  }
  .tabItem({
    Text("Artworks 💕 🙏 🌟")
  })
  
  NavigationView {
    ArtList(artworks: $artworks, tabTitle: "Unvisited Artworks", hideVisited: true)
    DetailView(artwork: artworks[0])
  }
  .tabItem({ Text("Unvisited Artworks") })
}

The first tab is the unfiltered list, and the second tab is the list of unvisited artworks. The tabItem modifier specifies the label on each tab.

Start Live-Preview to experience your alternative app:

In the Unvisited Artworks tab, use the context menu to add a reaction to an artwork: it disappears from this list, because it’s no longer unvisited!

Note: To launch the app with this view, open SceneDelegate.swift and replace let contentView = ContentView() with let contentView = ArtTabView().

Displaying a Modal Sheet

Another feature missing from this app is a map — you want to visit this artwork, but where is it, and how do you get there?

SwiftUI doesn’t have a map primitive view, but there’s one in Apple’s “Interfacing With UIKit” tutorial. I’ve adapted it, adding a pin annotation, and included it in the starter project.

UIViewRepresentable Protocol

Open MapView.swift: It’s A view that hosts an MKMapView. All the code in makeUIView and updateUIView is standard MapKit stuff. The SwiftUI magic is in the UIViewRepresentable protocol and its required methods — you guessed it: makeUIView and updateUIView. This shows how easy it is to display a UIKit view in a SwiftUI project. It works for any of your custom UIKit views, too.

Now try previewing the MapView (Option-Command-P). Well, it’s trying to show a map, but it’s just not there. Here’s the trick: You must start Live Preview to see the map:

The preview uses artData[5].coordinate as sample data, so the map pin shows the location of the Honolulu Zoo Elephant Exhibit, where you can visit the Giraffe sculpture.

Adding a Button

Now head back to DetailView.swift, which needs a button to show the map. You could put one in the navigation bar, but right next to the artwork’s location is also a logical place to put the show-map button.

To place a Button alongside the Text view, you need an HStack. Make sure the canvas is open (Option-Command-Return), then Command-click Text in this line of code:

Text(artwork.locationName)

And select Embed in HStack from the menu:

Now, to place the button to the left of the location text, you’ll add it before the Text in the HStack: Open the Library (Shift-Command-L), and drag a Button into your code, above Text(artwork.locationName).

Note: While dragging the Button, hover near Text until a new line opens above Text, then release the Button.

Your code now looks like this:

Button(action: {}) {
  Text("Button")
}
Text(artwork.locationName)
  .font(.subheadline)

Text("Button") is the button’s label. Change it to:

Image(systemName: "mappin.and.ellipse")

And refresh the preview:

Note: This system image is from Apple’s new SFSymbols collection. To see the full set, download and install the SF Symbols app from Apple. At least a couple of symbols seem to be deprecated: I tried to use mappin.circle and its filled version, but they didn’t appear.

So the label looks right. Now, what should the button’s action do?