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.
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
30 mins
- Getting Started
- SwiftUI Basics in a Nutshell
- Declarative App Development
- Declaring Views
- Creating a Basic List
- The List id Parameter
- Starting Debug
- 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
- Declaring Data Dependencies
- Guiding Principles
- Tools for Data Flow
- Adding a Navigation Bar Button
- Reacting to Artwork
- Adding a Context Menu
- Bonus Section: Eager Evaluation
- Where to Go From Here?
Declaring Data Dependencies
You’ve seen how easy it is to declare your UI. Now it’s time to learn about the other big feature of SwiftUI: declarative data dependencies.
Guiding Principles
SwiftUI has two guiding principles for managing how data flows through your app:
- Data access = dependency: Reading a piece of data in your view creates a dependency for that data in that view. Every view is a function of its data dependencies — its inputs or state.
- Single source of truth: Every piece of data that a view reads has a source of truth, which is either owned by the view or external to the view. Regardless of where the source of truth lies, you should always have a single source of truth. You give read-write access to a source of truth by passing a binding to it.
In UIKit, the view controller keeps the model and view in sync. In SwiftUI, the declarative view hierarchy plus this single source of truth means you no longer need the view controller.
Tools for Data Flow
SwiftUI provides several tools to help you manage the flow of data in your app.
Property wrappers augment the behavior of variables. SwiftUI-specific wrappers — @State, @Binding, @ObservedObject and @EnvironmentObject — declare a view’s dependency on the data represented by the variable.
Each wrapper indicates a different source of data:
-
@Statevariables are owned by the view.@State varallocates persistent storage, so you must initialize its value. Apple advises you to mark theseprivateto emphasize that a@Statevariable is owned and managed by that view specifically. -
@Bindingdeclares dependency on a@State varowned by another view, which uses the$prefix to pass a binding to this state variable to another view. In the receiving view,@Binding varis a reference to the data, so it doesn’t need initialization. This reference enables the view to edit the state of any view that depends on this data. -
@ObservedObjectdeclares dependency on a reference type that conforms to theObservableObjectprotocol: It implements anobjectWillChangeproperty to publish changes to its data. -
@EnvironmentObjectdeclares dependency on some shared data — data that’s visible to all views in the app. It’s a convenient way to pass data indirectly, instead of passing data from parent view to child to grandchild, especially if the child view doesn’t need it.
Now move on to practice using @State and @Binding for navigation.
Adding a Navigation Bar Button
If an Artwork has 💕, 🙏 or 🌟 as its reaction value, it indicates the user has visited this artwork. A useful feature would let users hide their visited artworks so they can choose one of the others to visit next.
In this section, you’ll add a button to the navigation bar to show only artworks the user hasn’t visited yet.
Start by displaying the reaction value in the list row, next to the artwork title: Change Text(artwork.title) to the following:
Text("\(artwork.reaction) \(artwork.title)")
Refresh the preview to see which items have a nonempty reaction:
Now, add these properties at the top of ContentView:
@State private var hideVisited = false
var showArt: [Artwork] {
hideVisited ? artworks.filter { $0.reaction.isEmpty } : artworks
}
The @State property wrapper declares a data dependency: Changing the value of this hideVisited property triggers an update to this view. In this case, changing the value of hideVisited will hide or show the already-visited artworks. You initialize this to false, so the list displays all of the artworks when the app launches.
The computed property showArt is all of artworks if hideVisited is false; otherwise, it’s a sub-array of artworks, containing only those items in artworks that have an empty-string reaction.
Now, replace the first line of the List declaration with:
List(showArt) { artwork in
Now add a navigationBarItems modifier to List after .navigationBarTitle("Artworks"):
.navigationBarItems(
trailing: Toggle(isOn: $hideVisited) { Text("Hide Visited") })
You’re adding a navigation bar item on the right side (trailing edge) of the navigation bar. This item is a Toggle view with label “Hide Visited”.
You pass the binding $hideVisited to Toggle. A binding allows read-write access, so Toggle will be able to change the value of hideVisited whenever the user taps it. This change will flow through to update the List view.
Start Live-Preview to see this working:
Tap the toggle to see the visited artworks disappear: Only the artworks with empty-string reactions remain. Tap again to see the visited artworks reappear.
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 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)
}
}
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) {
self.artworks = artworks.map { artwork in
guard artwork.id == item.id else { return artwork }
let updateArtwork = Artwork(
artist: item.artist,
description: item.description,
locationName: item.locationName,
discipline: item.discipline,
title: item.title,
imageName: item.imageName,
coordinate: item.coordinate,
reaction: reaction
)
return updateArtwork
}
}
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 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? 💕 🙏 🌟!


