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.

Leave a rating/review
Download materials
Save for later
Share

Also, this tutorial assumes you’re comfortable with using Xcode to develop iOS apps. You need Xcode 14. Some familiarity with UIKit and SwiftUI will be helpful.

Update note: Fabrizio Brancati updated this tutorial for Xcode 14 and iOS 16. Audrey Tam wrote the original.

Getting Started

Use the Download Materials button at the top or bottom of this tutorial to download the starter project. Open the PublicArt project in the Starter folder. You’ll build a master-detail app using the Artwork.swift file already included in this project.

SwiftUI Basics in a Nutshell

SwiftUI lets you ignore Interface Builder and storyboards without having to write step-by-step instructions for laying out your UI. You can preview a SwiftUI view side-by-side with its code — a change to one side will update the other side, so they’re always in sync. There aren’t any identifier strings to get wrong. And it’s code, but a lot less than you’d write for UIKit, so it’s easier to understand, edit and debug. What’s not to love?

The canvas preview means you don’t need a storyboard. The subviews keep themselves updated, so you also don’t need a view controller. And live preview means you rarely need to launch the simulator.

Note: Check out SwiftUI: Getting Started to learn more about the mechanics of developing a single-view SwiftUI app in Xcode.

SwiftUI doesn’t replace UIKit. Like Swift and Objective-C, you can use both in the same app. At the end of this tutorial, you’ll see how easy it is to use a UIKit view in a SwiftUI app.

Declarative App Development

SwiftUI enables you to do declarative app development: You declare both how you want the views in your UI to look and also what data they depend on. The SwiftUI framework takes care of creating views when they should appear and updating them whenever data they depend on changes. It recomputes the view and all its children, then renders what has changed.

A view’s state depends on its data, so you declare the possible states for your view and how the view appears for each state — how the view reacts to data changes or how data affect the view. Yes, there’s a definite reactive feeling to SwiftUI! If you’re already using one of the reactive programming frameworks, you’ll have an easier time picking up SwiftUI.

Declaring Views

A SwiftUI view is a piece of your UI: You combine small views to build larger views. There are lots of primitive views like Text and Color, which you can use as building blocks for your custom views.

Open ContentView.swift, and ensure its canvas is open (Option-Command-Return). Then click the + button or press Command-Shift-L to open the Library:

Library of primitive views

The first tab lists primitive views for layout and control, plus Layouts, Other Views and Paints. Many of these, especially the control views, are familiar to you as UIKit elements, but some are unique to SwiftUI.

Library of primitive modifiers

The second tab lists modifiers for layout, effects, text, events and other purposes, including presentation, environment and accessibility. A modifier is a method that creates a new view from the existing view. You can chain modifiers like a pipeline to customize any view.

SwiftUI encourages you to create small reusable views, then customize them with modifiers for the specific context where you use them. Don’t worry. SwiftUI collapses the modified view into an efficient data structure, so you get all this convenience with no visible performance hit.

Creating a Basic List

Start by creating a basic list for the master view of your master-detail app. In a UIKit app, this would be a UITableViewController.

Edit ContentView to look like this:

struct ContentView: View {
  let disciplines = ["statue", "mural", "plaque"]
  var body: some View {
    List(disciplines, id: \.self) { discipline in
      Text(discipline)
    }
  }
}

You create a static array of strings and display them in a List view, which iterates over the array, displaying whatever you specify for each item. And the result looks like a UITableView!

Ensure your canvas is open, then refresh the preview (click the Resume button or press Option-Command-P):

A basic list of strings

There’s your list, like you expected to see. How easy was that? No UITableViewDataSource methods to implement, no UITableViewCell to configure, and no UITableViewCell identifier to misspell in tableView(_:cellForRowAt:)!

The List id Parameter

The parameters of List are the array, which is obvious, and id, which is less obvious. List expects each item to have an identifier, so it knows how many unique items there are (instead of tableView(_:numberOfRowsInSection:)). The argument \.self tells List that each item is identified by itself. This is OK as long as the item’s type conforms to the Hashable protocol, which all the built-in types do.

Take a closer look at how id works: Add another "statue" to disciplines:

let disciplines = ["statue", "mural", "plaque", "statue"]

Refresh the preview: all four items appear. But, according to id: \.self, there are only three unique items. A breakpoint might shed some light.

Add a breakpoint at Text(discipline).

Starting Debug

Run the simulator, and the app execution stops at your breakpoint, and the Variables View displays discipline:

First stop at breakpoint: discipline = statue

Click the Continue program execution button: Now discipline = "statue" again.

Click Continue again to see discipline = "mural". After tapping on Continue, you see the same value, mural, again. Same happens in the next two clicks on the Continue as well with discipline = "plaque". Then one final Continue displays the list of four items. So no — execution doesn’t stop for the fourth list item.

What you’ve seen is: execution visited each of the three unique items twice. So List does see only three unique items. Later, you’ll learn a better way to handle the id parameter. But first, you’ll see how easy it is to navigate to a detail view.

Stop the simulator execution and remove the breakpoint.

Navigating to the Detail View

You’ve seen how easy it is to display the master view. It’s about as easy to navigate to the detail view.

First, embed List in a NavigationStack, like this:

NavigationStack {
  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 items such as the navigation bar title. Notice .navigationBarTitle modifies List, not NavigationStack. You can declare more than one view in a NavigationStack, and each can have its own .navigationBarTitle.

Refresh the preview to see how this looks:

List in NavigationStack with navigationBarTitle

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.