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.