Most apps are made of lists and the screens behind them. Mail shows a list of messages that open into a message; Settings is a list of lists; a shop lists products that open into details. SwiftUI's List builds these scrolling, platform-styled lists from your data in a few lines, and NavigationStack handles moving between screens, complete with the back button and swipe-to-go-back gesture users expect.
Lists depend on the protocols and state you've already learned. Each item needs a stable identity, provided by Identifiable, so SwiftUI can animate insertions and deletions correctly. Editing a list means changing an array held in @State or an @Observable model, and the list updates itself.
In this lesson you'll build a list from data, add sections, swipe actions and search, navigate to detail screens, present sheets and alerts, and organise an app into tabs.
List and ForEach
struct Contact: Identifiable {
let id = UUID()
let name: String
}
struct ContactsView: View {
let contacts = [Contact(name: "Ada"), Contact(name: "Grace")]
var body: some View {
List(contacts) { contact in
Text(contact.name)
}
}
}- Data passed to
ListorForEachmust be Identifiable, or you supply an id key path such asid: \.selffor simple values. ForEachrepeats views without making its own list, so it can be mixed with fixed rows insideList, or used inside stacks.Section("Title") { ... }groups rows with a header.
Sheets, alerts and tabs
Sheets and alerts are driven by state: set a Boolean, and SwiftUI presents them.
@State private var showingNew = false
Button("New") { showingNew = true }
.sheet(isPresented: $showingNew) {
NewContactView()
}.alert("Delete?", isPresented: $confirming) { Button("Delete", role: .destructive) { ... } }shows an alert.- Inside a sheet,
@Environment(\.dismiss) private var dismissanddismiss()close it. TabViewholds the app's top-level sections, each with.tabItem { Label("Home", systemImage: "house") }.
SwiftUI Navigation Syntax
Lists and navigation
List(items) { item in Text(item.name) }
List { ForEach(items) { Text($0.name) }.onDelete { items.remove(atOffsets: $0) } }
Section("Today") { ... }
NavigationStack { ... .navigationTitle("Inbox") }
NavigationLink("Open", value: item)
.navigationDestination(for: Item.self) { DetailView(item: $0) }
.sheet(isPresented: $showing) { EditView() }
.alert("Title", isPresented: $alerting) { Button("OK") { } }
.searchable(text: $query)
TabView { HomeView().tabItem { Label("Home", systemImage: "house") } }SwiftUI Navigation Summary
- List and ForEach build rows from Identifiable data; Section groups them.
- onDelete and swipeActions add editing gestures; changes go to the underlying array.
- NavigationStack with NavigationLink and navigationDestination moves between screens.
- Sheets and alerts appear when a bound Boolean becomes true; dismiss closes a sheet.
- searchable adds a search field; TabView organises top-level sections.
SwiftUI Navigation Examples
Each example shows the code, the output it produces and what each part does.
A task list with delete and add
import SwiftUI
struct TodoItem: Identifiable {
let id = UUID()
var title: String
var done = false
}
struct TasksView: View {
@State private var tasks = [TodoItem(title: "Buy milk"), TodoItem(title: "Call Sam")]
@State private var newTitle = ""
var body: some View {
List {
Section("Add") {
TextField("New task", text: $newTitle)
.onSubmit {
tasks.append(TodoItem(title: newTitle))
newTitle = ""
}
}
Section("To do (\(tasks.count))") {
ForEach($tasks) { $task in
Toggle(task.title, isOn: $task.done)
}
.onDelete { tasks.remove(atOffsets: $0) }
}
}
}
}[Preview: an "Add" section with a text field, then "To do (2)" with toggles for "Buy milk" and "Call Sam"; swiping a row left reveals Delete]Line-by-line explanation
- Lines 3 to 7Each task has a stable UUID created once, when the task is made.
- Lines 17 to 20Pressing return adds a task; the list updates because tasks is state.
- Lines 23 to 25ForEach over $tasks gives each row a binding, so a Toggle can edit that task.
- Line 26onDelete adds swipe to delete and removes the rows from the array.
Navigation to a detail screen
import SwiftUI
struct Recipe: Identifiable, Hashable {
let id: Int
let name: String
let minutes: Int
}
struct RecipesView: View {
let recipes = [Recipe(id: 1, name: "Pancakes", minutes: 20),
Recipe(id: 2, name: "Curry", minutes: 45)]
var body: some View {
NavigationStack {
List(recipes) { recipe in
NavigationLink(value: recipe) {
LabeledContent(recipe.name, value: "\(recipe.minutes) min")
}
}
.navigationTitle("Recipes")
.navigationDestination(for: Recipe.self) { recipe in
Text("Cook \(recipe.name) for \(recipe.minutes) minutes")
.navigationTitle(recipe.name)
}
}
}
}[Preview: a "Recipes" screen listing "Pancakes 20 min" and "Curry 45 min"; tapping a row slides in a detail screen with a back button]Line-by-line explanation
- Line 3Recipe is Hashable so it can be pushed as a navigation value.
- Lines 16 to 18A NavigationLink with a value and a custom label.
- Line 20The title appears in the navigation bar.
- Lines 21 to 24navigationDestination says which screen to show for a Recipe.
Search, a sheet and tabs
import SwiftUI
struct CitiesView: View {
let cities = ["Berlin", "Lisbon", "London", "Madrid", "Paris"]
@State private var query = ""
@State private var showingAbout = false
var results: [String] {
query.isEmpty ? cities : cities.filter { $0.localizedCaseInsensitiveContains(query) }
}
var body: some View {
NavigationStack {
List(results, id: \.self) { Text($0) }
.navigationTitle("Cities")
.searchable(text: $query)
.toolbar {
Button("About") { showingAbout = true }
}
.sheet(isPresented: $showingAbout) {
Text("Five cities to visit").presentationDetents([.medium])
}
}
}
}
struct AppTabs: View {
var body: some View {
TabView {
CitiesView().tabItem { Label("Cities", systemImage: "building.2") }
Text("Saved").tabItem { Label("Saved", systemImage: "heart") }
}
}
}[Preview: a tab bar with Cities and Saved; the Cities tab shows a searchable list, and typing "lo" leaves "Lisbon" and "London"]Line-by-line explanation
- Lines 8 to 10A computed property filters the list from state, so results always match the query.
- Line 14Strings use id: \.self because they aren't Identifiable.
- Line 16searchable adds a search field bound to query.
- Lines 20 to 22The sheet appears when showingAbout becomes true; presentationDetents makes it half height.
- Lines 29 to 31TabView shows each section with a tab bar label and icon.
Common SwiftUI Navigation Mistakes
Using indices as ids
ForEach(0..<items.count, id: \.self) { i in Text(items[i].name) } // breaks after deletionsForEach(items) { item in Text(item.name) } // items are IdentifiableA new id every time body runs
struct Row: Identifiable {
var id: UUID { UUID() } // a different id on every access
let title: String
}struct Row: Identifiable {
let id = UUID() // created once
let title: String
}Navigation links without a stack
List(items) { NavigationLink($0.name, value: $0) } // nothing happens on tapNavigationStack {
List(items) { NavigationLink($0.name, value: $0) }
.navigationDestination(for: Item.self) { DetailView(item: $0) }
}SwiftUI Navigation in Real Projects
Lists and navigation in shipping apps
Almost every iPhone app's main screen is a List or a scrolling stack inside a NavigationStack, under a TabView. Apps add pull to refresh with .refreshable { await reload() }, search with .searchable, and editing with swipe actions. Storing the navigation path in state lets an app open a specific screen from a notification or a link, a feature called deep linking.
SwiftUI Navigation FAQ
What is the difference between List and ForEach in SwiftUI?
List is a scrolling container with the platform's row styling, separators and swipe actions. ForEach just repeats a view for each item and has no appearance of its own. You often put ForEach inside a List to mix generated rows with fixed ones, or inside a stack for a custom layout.
What is the difference between NavigationView and NavigationStack?
NavigationView is the older container, deprecated from iOS 16. NavigationStack replaces it for push-style navigation and supports value-based links and a bindable path for navigating in code. Use NavigationStack in new apps, and NavigationSplitView for multi-column layouts on iPad and Mac.
Why does my List need Identifiable items?
SwiftUI needs to tell rows apart so it can animate insertions and deletions and keep the right row selected. Conforming to Identifiable with a stable id gives each item that identity. For simple values like unique strings, you can pass id: \.self instead.
How do I go back to the root view in SwiftUI?
Bind the NavigationStack to a path array in state, then empty it with path.removeAll(). Every pushed screen is removed and the stack shows its root view again.
How do I dismiss a sheet in SwiftUI?
Inside the sheet, read the dismiss action with @Environment(\.dismiss) private var dismiss and call dismiss(), for example from a Done button. Alternatively, set the Boolean that presented the sheet back to false in the parent.
SwiftUI Navigation Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the struct so it can be shown in a List without an id argument.
struct Song: {
let id = UUID()
let title: String
}
Which container provides the navigation bar and back button?
How is a sheet usually shown?
cities is ["Berlin", "Lisbon", "London"] and the filter keeps names where $0.localizedCaseInsensitiveContains("ON"). How many remain?
Why does a value used with navigationDestination(for:) need to be Hashable?
SwiftUI Navigation Quiz
5 questions to check what you learned.
What does onDelete add to a ForEach inside a List?
- Swipe to delete on each row
- A delete button in the toolbar
- An alert
- Nothing visible
How do you close a sheet from inside it?
- dismissSheet()
- Read @Environment(\.dismiss) and call dismiss()
- Set the view to nil
- Use a NavigationLink
How do you show a list of plain Strings?
- Strings can't be shown in lists
- Pass id: \.self, because Strings aren't Identifiable
- Wrap each in a UUID
- Convert them to Ints
What does .searchable(text: $query) do?
- Searches the web
- Sorts the list
- Adds a search field bound to query, which you use to filter the data
- Highlights rows
What is TabView for?
- Organising an app's top-level sections, switched with a tab bar
- Showing tables of data
- Indenting text
- Pushing detail screens