Lists, ForEach & Navigation
Reviewed & published by Brayan K
Most apps show lists of data and let you tap a row to see details. You'll build scrollable lists with List, generate rows from data with ForEach and Identifiable, and wire up screen-to-screen navigation with NavigationStack and NavigationLink.
Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Building iOS Apps with SwiftUI • Intermediate
What You'll Learn in This Lesson
- • Show scrollable rows with List
- • Generate rows from data with ForEach
- • Give models identity with Identifiable
- • Use id: \.self for plain values
- • Push screens with NavigationStack + NavigationLink
1️⃣ List — Scrollable Rows
List renders a scrollable, platform-styled column of rows, complete with separators. The simplest form just contains a few static views.
import SwiftUI
// List shows a scrollable, platform-styled column of rows. The simplest form
// takes a static set of child views.
struct MenuList: View {
var body: some View {
List {
Text("Espresso")
Text("Latte")
Text("Cappuccino")
}
}
}2️⃣ ForEach & Identifiable — Rows from Data
Real lists come from data. ForEach builds one row per element. For SwiftUI to track rows correctly, each element needs a stable identity — conform your model to Identifiable by giving it an id.
import SwiftUI
// To build rows from DATA, your model conforms to Identifiable: it has an
// 'id' that uniquely and stably identifies each value.
struct Task: Identifiable {
let id = UUID()
let title: String
}
struct TaskList: View {
let tasks = [
Task(title: "Buy milk"),
Task(title: "Walk dog"),
Task(title: "Write code")
]
var body: some View {
// ForEach builds one row per task; List makes it scrollable.
List {
ForEach(tasks) { task in
Text(task.title)
}
}
}
}For plain values like strings that aren't Identifiable, pass an id key path instead.
import SwiftUI
// If your elements are NOT Identifiable (e.g. plain strings), give ForEach an
// id key path so SwiftUI can tell rows apart.
struct ColorList: View {
let colors = ["Red", "Green", "Blue"]
var body: some View {
List {
// 'id: \.self' uses each string itself as its identity
ForEach(colors, id: \.self) { color in
Text(color)
}
}
}
}3️⃣ NavigationStack & NavigationLink
Wrap your content in a NavigationStack to enable pushing detail screens. A NavigationLink shows a row that, when tapped, pushes its destination onto the stack. Add a title with .navigationTitle(...).
import SwiftUI
struct Fruit: Identifiable {
let id = UUID()
let name: String
}
// NavigationStack manages pushed screens. A NavigationLink pushes a
// destination when its row is tapped.
struct FruitBrowser: View {
let fruits = [Fruit(name: "Apple"), Fruit(name: "Pear")]
var body: some View {
NavigationStack {
List {
ForEach(fruits) { fruit in
NavigationLink(fruit.name) {
// destination pushed when tapped
Text("All about \(fruit.name)")
}
}
}
.navigationTitle("Fruits")
}
}
}Your turn. Fill in the blanks to build a navigable library list.
// 🎯 YOUR TURN — fill in each ___ then preview in Xcode.
import SwiftUI
struct Book: ___ { // 👉 protocol that requires an 'id'
let id = UUID()
let title: String
}
struct Library: View {
let books = [Book(title: "Swift 101"), Book(title: "SwiftUI Deep Dive")]
var body: some View {
___ { // 👉 the container that manages pushed screens
List {
___(books) { book in // 👉 loops over the books
NavigationLink(book.title) {
Text("Reading \(book.title)")
}
}
}
.navigationTitle("Library")
}
}
}Pro Tips
- 💡 Prefer a real id. Conform to Identifiable with a stable id rather than id: \.self when values might repeat.
- 💡 Keep rows in their own view. Extract a TaskRow view to keep body readable.
- 💡 Use NavigationStack (iOS 16+) over the older NavigationView for new code.
Common Errors (and the fix)
- "Referencing initializer requires 'X' conform to 'Identifiable'" — give your model an id and conform to Identifiable, or pass id: \.self.
- Tapping a link does nothing — the NavigationLink isn't inside a NavigationStack. Wrap your content.
- Rows animate wrongly on insert/delete — your ids aren't stable (e.g. recomputed each render). Use a persistent unique id.
Mini-Challenge: Contacts App
Define a Contact: Identifiable struct with a name and phone number. Build a List inside a NavigationStack that uses ForEach to show each contact's name, with a NavigationLink to a detail screen showing the full name and number. Give the list the title "Contacts".
🎉 Lesson Complete!
- ✅ List shows scrollable rows; ForEach builds rows from data
- ✅ Identifiable gives each element a stable id
- ✅ Use id: \.self for plain unique values
- ✅ NavigationStack + NavigationLink push detail screens
Practice quiz
What does the List view do?
- Draws a single row
- Shows a scrollable column of rows
- Sorts an array
- Stores data
Answer: Shows a scrollable column of rows. List presents data in a scrollable, platform-styled column of rows.
What is ForEach used for in SwiftUI?
- Looping over a collection to build a view per element
- Running a background task
- Repeating an animation
- Filtering data
Answer: Looping over a collection to build a view per element. ForEach creates one view for each element of a collection.
Why must ForEach elements be identifiable?
- For sorting
- So SwiftUI can tell rows apart to update them correctly
- To save memory
- It is optional and never needed
Answer: So SwiftUI can tell rows apart to update them correctly. Stable identity lets SwiftUI track, insert, move, and delete the right rows efficiently.
What does conforming to Identifiable require?
- A name property
- An id property with a stable, unique value
- A description method
- Nothing
Answer: An id property with a stable, unique value. Identifiable requires an id property that uniquely and stably identifies each instance.
How can you give ForEach an id without Identifiable?
- ForEach(items, key: ...)
- ForEach(items, id: \.self) or id: \.someProperty
- ForEach(items).id()
- You cannot
Answer: ForEach(items, id: \.self) or id: \.someProperty. Pass an id key path, e.g. ForEach(items, id: \.self), when the type isn't Identifiable.
What is NavigationStack?
- A list of tabs
- A layout stack
- A container that manages a stack of pushed screens
- An animation
Answer: A container that manages a stack of pushed screens. NavigationStack (iOS 16+) manages a navigation stack, pushing and popping detail screens.
What does NavigationLink do?
- Opens a web URL
- Pushes a destination view when tapped
- Dismisses a sheet
- Reorders a list
Answer: Pushes a destination view when tapped. NavigationLink presents a destination view by pushing it onto the navigation stack when tapped.
Where must a NavigationLink live to work?
- Anywhere at all
- Inside a NavigationStack (or NavigationView)
- Only inside a Button
- Inside a ZStack
Answer: Inside a NavigationStack (or NavigationView). A NavigationLink needs an enclosing NavigationStack to have a stack to push onto.
How do you add a title to a navigation screen?
- .title("X")
- .header("X")
- .name("X")
- .navigationTitle("X")
Answer: .navigationTitle("X"). Apply .navigationTitle("X") to the content inside the NavigationStack to set the bar title.
What is a clean way to combine a List with dynamic rows?
- List with a ForEach over your data
- A ZStack of Texts
- Many nested VStacks
- A single Text
Answer: List with a ForEach over your data. Putting a ForEach inside a List builds one row per element, the standard pattern for dynamic lists.
Continue this course
- Previous: Layout: Stacks, Frames & GeometryReader
- Next: Forms & User Input — Form, TextField, Toggle, Picker and Stepper bound to state
Frequently asked questions
What is the difference between List and ForEach?
List is a container that renders a scrollable, styled column of rows with separators and platform behaviour. ForEach is a loop that generates one view per element of a collection. You usually nest a ForEach inside a List to turn an array of data into rows, but ForEach also works inside VStack and other containers.
What is Identifiable and why does ForEach need it?
Identifiable is a protocol requiring an id property that uniquely and stably identifies each value. ForEach uses that identity to track rows across updates — so when you insert, move, or delete data, SwiftUI updates exactly the right rows with correct animations. Without stable identity it can't tell which row changed.
How do I use ForEach with plain values like strings?
Plain types like String aren't Identifiable, so pass an id key path: ForEach(colors, id: \.self) uses each value itself as the identity. Use id: \.self only when values are unique; otherwise give your model an explicit unique id property and conform to Identifiable.
What is NavigationStack and NavigationLink?
NavigationStack (iOS 16+) is a container that manages a stack of screens, pushing and popping detail views. NavigationLink is a tappable row that, when tapped, pushes a destination view onto that stack. The link must live inside a NavigationStack so there's a stack to push onto.
How do I set a screen's navigation title?
Apply the .navigationTitle("Your Title") modifier to the content inside the NavigationStack — typically on the List or top container. It sets the title shown in the navigation bar. You can also add toolbar buttons with the .toolbar modifier.