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.
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
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
SwiftUI Tutorial: Navigation
45 mins
- Getting Started
- SwiftUI Basics in a Nutshell
- Declarative App Development
- Declaring Views
- Creating a Basic List
- The List id Parameter
- Starting Debug Preview
- Navigating to the Detail View
- Creating a Navigation Link
- Revisiting Honolulu Public Artworks
- Creating Unique id Values With UUID()
- Conforming to Identifiable
- Showing More Detail
- Handling Split View
- Declaring Data Dependencies
- Guiding Principles
- Tools for Data Flow
- Adding a Navigation Bar Button
- Reacting to Artwork
- Adding a Context Menu
- Creating a Tab View App
- Displaying a Modal Sheet
- UIViewRepresentable Protocol
- Adding a Button
- Showing a Modal Sheet
- Dismissing the Modal Sheet
- Bonus Section: Eager Evaluation
- Where to Go From Here?
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)
}
}
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.
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 ContentView
— not 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!
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)
.
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:
mappin.circle
and its filled version, but they didn’t appear.
So the label looks right. Now, what should the button’s action
do?