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?
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.