Lists and Navigation in SwiftUI: `List`, `NavigationStack`, and Detail Views
Almost every app on your iPhone follows the same pattern: you see a scrollable list of items, tap one, and a detail screen slides in from the right. The Settings app, Mail, Notes, Music — they all work this way. Pixar’s own Monsters, Inc. has a similar structure: a floor full of doors (the list), and behind each door is a unique child’s room (the detail view).
This guide covers how to build that pattern with List, NavigationStack, and NavigationLink. You will also learn
ForEach for dynamic data and programmatic navigation for when you need to push views from code. We won’t cover tab
bars or toolbar customization — those are separate topics.
This post assumes you are comfortable with state management in SwiftUI.
What You’ll Learn
- What Is a List?
- Creating a Basic List
- Dynamic Lists with ForEach
- Adding Navigation with NavigationStack
- Building Detail Views
- Programmatic Navigation
- Common Mistakes
- What’s Next?
What Is a List?
A List is a scrollable container that displays rows of data vertically. It automatically adds the familiar iOS styling
— rounded row backgrounds, separators between rows, and smooth scrolling.
Apple Docs: List — SwiftUI
Think of a List like the door vault in Monsters, Inc. — a long conveyor of doors, each one representing a unique
entry. You scroll through them and pick the one you want.
Creating a Basic List
The simplest list contains static rows — content you write directly in the code:
List {
Text("Toy Story")
Text("Finding Nemo")
Text("Up")
Text("WALL-E")
Text("Coco")
}
┌─────────────────────────┐
│ Toy Story │
├─────────────────────────┤
│ Finding Nemo │
├─────────────────────────┤
│ Up │
├─────────────────────────┤
│ WALL-E │
├─────────────────────────┤
│ Coco │
└─────────────────────────┘
Each Text view becomes a row. SwiftUI adds separators and scrolling behavior automatically. You don’t need to manage
scroll positions or row recycling — List handles it all.
Dynamic Lists with ForEach
Static lists are fine for a few items, but real apps display data from arrays, databases, or network responses.
ForEach generates rows dynamically from a collection.
struct PixarMovie: Identifiable {
let id = UUID()
let title: String
let year: Int
}
let movies = [
PixarMovie(title: "Toy Story", year: 1995),
PixarMovie(title: "Finding Nemo", year: 2003),
PixarMovie(title: "Up", year: 2009),
PixarMovie(title: "Inside Out", year: 2015),
PixarMovie(title: "Coco", year: 2017),
]
List {
ForEach(movies) { movie in
HStack {
Text(movie.title)
Spacer()
Text("\(movie.year)")
.foregroundStyle(.secondary)
}
}
}
┌──────────────────────────────┐
│ Toy Story 1995 │
├──────────────────────────────┤
│ Finding Nemo 2003 │
├──────────────────────────────┤
│ Up 2009 │
├──────────────────────────────┤
│ Inside Out 2015 │
├──────────────────────────────┤
│ Coco 2017 │
└──────────────────────────────┘
Two important details here:
-
Identifiableprotocol —ForEachneeds a way to tell rows apart. By conformingPixarMovietoIdentifiableand adding anidproperty, each row gets a unique identity. This lets SwiftUI animate insertions, deletions, and moves correctly. -
The closure —
ForEach(movies) { movie in ... }loops through the array and creates a view for each element. Themovieparameter gives you access to the current item.
Tip: You can also pass an
idkey path instead of conforming toIdentifiable:ForEach(movies, id: \.title) { movie in ... }. This works when your data doesn’t have a dedicatedidproperty.
Adding Navigation with NavigationStack
A list becomes truly useful when tapping a row takes you to a detail screen. NavigationStack provides the container
for this push-and-pop navigation pattern.
Apple Docs: NavigationStack — SwiftUI
Wrap your List inside a NavigationStack and use NavigationLink to make rows tappable:
Apple Docs: NavigationLink — SwiftUI
NavigationStack {
List {
ForEach(movies) { movie in
NavigationLink(value: movie) {
Text(movie.title)
}
}
}
.navigationTitle("Pixar Movies")
.navigationDestination(for: PixarMovie.self) { movie in
MovieDetailView(movie: movie)
}
}
┌──────────────────────────────┐
│ Pixar Movies (nav) │
├──────────────────────────────┤
│ Toy Story > │
├──────────────────────────────┤
│ Finding Nemo > │
├──────────────────────────────┤
│ Up > │
├──────────────────────────────┤
│ Inside Out > │
├──────────────────────────────┤
│ Coco > │
└──────────────────────────────┘
Here is what each piece does:
NavigationStackwraps the entire view and manages a navigation path (a stack of screens).NavigationLink(value: movie)makes a row tappable and associates the movie data with the tap..navigationTitle("Pixar Movies")sets the large title at the top..navigationDestination(for: PixarMovie.self)tells SwiftUI what view to push when aPixarMovievalue is tapped.
Note: For
.navigationDestinationto work with your custom type, the type must conform toHashable. AddHashableconformance to your struct:
struct PixarMovie: Identifiable, Hashable {
let id = UUID()
let title: String
let year: Int
}
Building Detail Views
The detail view is the screen that appears when you tap a row. It receives the selected item and displays its full information:
struct MovieDetailView: View {
let movie: PixarMovie
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(movie.title)
.font(.largeTitle)
.bold()
Text("Released: \(movie.year)")
.font(.title3)
.foregroundStyle(.secondary)
Spacer()
}
.padding()
.navigationTitle(movie.title)
.navigationBarTitleDisplayMode(.inline)
}
}
┌──────────────────────────────┐
│ < Back Toy Story │
├──────────────────────────────┤
│ │
│ Toy Story │
│ Released: 1995 │
│ │
│ │
│ │
└──────────────────────────────┘
SwiftUI automatically adds a Back button at the top-left showing the previous screen’s title. The
.navigationBarTitleDisplayMode(.inline) keeps the title compact in the navigation bar instead of using the large title
style.
Programmatic Navigation
Sometimes you need to navigate from code rather than from a user tap — for example, after saving data or completing a
task. NavigationStack supports this with a path binding.
struct MovieListView: View {
@State private var path: [PixarMovie] = []
var body: some View {
NavigationStack(path: $path) {
List {
ForEach(movies) { movie in
NavigationLink(value: movie) {
Text(movie.title)
}
}
Button("Jump to WALL-E") {
if let wallE = movies.first(where: {
$0.title == "WALL-E"
}) {
path.append(wallE)
}
}
}
.navigationTitle("Pixar Movies")
.navigationDestination(for: PixarMovie.self) { movie in
MovieDetailView(movie: movie)
}
}
}
}
The path array represents the navigation stack. When you append a movie to path, SwiftUI pushes the corresponding
detail view. When you remove items, SwiftUI pops views. Setting path = [] returns to the root.
This is powerful for deep linking, onboarding flows, or any scenario where navigation depends on logic rather than direct user taps.
Common Mistakes
Forgetting Identifiable on Your Data Model
ForEach requires each item to be uniquely identifiable. Without Identifiable (or an explicit id parameter), your
code won’t compile.
// ❌ Missing Identifiable — won't compile
struct Movie {
let title: String
}
ForEach(movies) { movie in Text(movie.title) }
// ✅ Add Identifiable with a unique id
struct Movie: Identifiable {
let id = UUID()
let title: String
}
ForEach(movies) { movie in Text(movie.title) }
Putting navigationTitle Outside NavigationStack
The .navigationTitle() modifier must be applied to a view inside the NavigationStack, not on the
NavigationStack itself.
// ❌ Title won't appear
NavigationStack {
List { /* ... */ }
}
.navigationTitle("Movies")
// ✅ Title applied inside the stack
NavigationStack {
List { /* ... */ }
.navigationTitle("Movies")
}
Using the Old NavigationView
NavigationView was deprecated in iOS 16. Always use NavigationStack for new projects. It supports type-safe
destinations, programmatic navigation, and deep linking — features that NavigationView lacks.
// ❌ Deprecated
NavigationView {
List { /* ... */ }
}
// ✅ Modern approach
NavigationStack {
List { /* ... */ }
}
What’s Next?
Listcreates scrollable, styled rows with minimal code.ForEachgenerates rows dynamically from collections — data must beIdentifiable.NavigationStackmanages a stack of screens with push-and-pop behavior.NavigationLinkmakes rows tappable;.navigationDestinationdefines what gets pushed.- Programmatic navigation uses a
pathbinding to control the stack from code.
You now have the fundamental building blocks of most iOS apps: views, layout, state, lists, and navigation. Ready to collect user input? Continue to Forms and User Input in SwiftUI to learn about text fields, toggles, pickers, and form validation.