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?
Creating a Navigation Link
NavigationStack
also enables NavigationLink
, which needs a destination
view and a label — like creating a segue in a storyboard, but without those pesky segue identifiers.
First, create your DetailView
. For now, 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 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, and add the .navigationDestination(for:destination:)
destination modifier:
List(disciplines, id: \.self) { discipline in
NavigationLink(value: discipline) {
Text(discipline)
}
}
.navigationDestination(for: String.self, destination: { discipline in
DetailView(discipline: discipline)
})
.navigationBarTitle("Disciplines")
There’s no prepare(for:sender:)
rigmarole — you 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:
Tap a row to show its detail view:
And zap, it works! Notice you get the usual back button, too.
But the view looks so plain — it doesn’t even have a title.
Add a title to the DetailView
:
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 NavigationStack
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:
Now you know how to create a basic master-detail app. You used String
objects, to avoid clutter that might obscure how lists and navigation work. But list items are usually instances of a model type 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 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 Artwork.swift
file add the following:
extension Artwork: Hashable {
static func == (lhs: Artwork, rhs: Artwork) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
This will let you use Artwork
inside a List
, because all items must be Hashable
.
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.
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 useful later!
Conforming to Identifiable
But there’s an even better way: Go back to Artwork.swift, and add this extension, outside the Artwork
struct:
extension Artwork: Identifiable { }
The id
property is all you need to make Artwork
conform to Identifiable
, and you’ve already added that.
Now you can avoid specifying id
parameter entirely:
List(artworks) { artwork in
Looks much neater now! Because Artwork
conforms to Identifiable
, List
knows it has an id
property and automatically uses this property for its id
argument.
Then, 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) { artwork in
NavigationLink(value: artwork) {
Text(artwork.title)
}
}
.navigationDestination(for: Artwork.self, destination: { artwork in
DetailView(artwork: artwork)
})
.navigationBarTitle("Artworks")
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)
}
}
You’ll soon create a separate file for DetailView
, but this will do for now.
Showing More Detail
Artwork
objects have lots of information you can display, so update your DetailView
to show more details.
First, create a new SwiftUI View file: Command-N ▸ iOS ▸ User Interface ▸ SwiftUI View. Name it DetailView.swift.
Replace import Foundation
with import SwiftUI
.
Delete DetailView
completely from ContentView.swift. You’ll replace it with a whole new view.
Add the following to DetailView.swift:
struct DetailView: View {
let artwork: Artwork
var body: some View {
VStack {
Image(artwork.imageName)
.resizable()
.frame(maxWidth: 300, maxHeight: 600)
.aspectRatio(contentMode: .fit)
Text("\(artwork.reaction) \(artwork.title)")
.font(.headline)
.multilineTextAlignment(.center)
.lineLimit(3)
Text(artwork.locationName)
.font(.subheadline)
Text("Artist: \(artwork.artist)")
.font(.subheadline)
Divider()
Text(artwork.description)
.multilineTextAlignment(.leading)
.lineLimit(20)
}
.padding()
.navigationBarTitle(Text(artwork.title), displayMode: .inline)
}
}
You’re displaying several views in a vertical layout, so everything is in a VStack
.
First is the Image
: The artData
images are all different sizes and aspect ratios, so you specify aspect-fit, and constrain the frame to at most 300 points wide by 600 points high. However, these modifiers won’t take effect unless you first modify the Image
to be resizable
.
You modify the Text
views to specify font size and multilineTextAlignment
, because some of the titles and descriptions are too long for a single line.
Finally, you add some padding around the stack.
You also need a preview, so add it:
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(artwork: artData[0])
}
}
Refresh the preview:
There’s Prince Jonah! In case you’re curious, Kalanianaole has seven syllables, four of them in the last six letters ;].
The navigation bar doesn’t appear when you preview or even live-preview DetailView
, because it doesn’t know it’s in a navigation stack.
Go back to ContentView.swift and tap a row to see the complete detail view: