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?
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 delete the 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.
Refresh the preview (Option-Command-P):
And it still works fine.
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 DetailView
in the new file with the DetailView
from ContentView.swift. Be sure to delete it from ContentView.swift.
The preview wants an artwork
argument, so add it:
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(artwork: artData[0])
}
}
Then, add lots of new things to the view:
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.
Refresh the preview:
And there’s Prince Jonah! In case you’re curious, there are seven syllables in Kalanianaole, 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 start Live Preview, then tap a row to see the complete detail view:
Handling Split View
So far, I’ve been showing you previews of the iPhone 8 scheme. But of course, you can view this on an iPad (or even on your Mac, as a Mac Catalyst app).
To see what this looks like on an iPad, select an iPad scheme, then restart the Live Preview:
Um, it’s blank!? Well, it’s an iPad, so SwiftUI shows you a split view. When an iPad is in portrait orientation, you have to swipe from the leading edge to open the master list view, then select an item:
To avoid showing a blank detail view on launch, simply add a specific DetailView
after the List
in ContentView
. Add the following after .navigationBarTitle("Artworks")
:
DetailView(artwork: artworks[0])
Refresh the preview (it doesn’t have to be live):
And now the split view loads with your default detail view.
Change the scheme back to an iPhone to see that this DetailView
doesn’t mess up your master list view!
NavigationView
with .navigationViewStyle(DoubleColumnNavigationViewStyle())
. If you don’t want split view at all, specify StackNavigationViewStyle()
to force the iPhone-style navigation stack behavior.
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:
-
@State
variables are owned by the view.@State var
allocates persistent storage, so you must initialize its value. Apple advises you to mark theseprivate
to emphasize that a@State
variable is owned and managed by that view specifically. -
@Binding
declares dependency on a@State var
owned by another view, which uses the$
prefix to pass a binding to this state variable to another view. In the receiving view,@Binding var
is 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. -
@ObservedObject
declares dependency on a reference type that conforms to theObservableObject
protocol: It implements anobjectWillChange
property to publish changes to its data. -
@EnvironmentObject
declares 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 then 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 yet visited.
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 non-empty reaction:
Now add these properties at the top of ContentView
:
@State private var hideVisited = false
var showArt: [Artwork] {
hideVisited ? artworks.filter { $0.reaction == "" } : 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, label: { 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. And 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.
There’s an alternative to this Toggle
you just implemented: a tab view! You won’t be surprised when I tell you it’s easy to implement a tab view in SwiftUI ;]. You’ll do this as soon as you set up a way for your users to react to an artwork, because it will make the unvisited tab more fun. ;]