Navigation & Multi-Screen Apps

iOS Development Fundamentals

Chapter 8 · Navigation & Multi-Screen Apps

Real apps have more than one screen. This chapter covers SwiftUI's two genuinely different ways of showing a second screen — pushing a new one onto a real navigation stack, and presenting one modally above the current screen — and how to pass real, typed data along with either.

NavigationStack & NavigationLink

NavigationStack, introduced in iOS 16, is Apple's real, current tool for hierarchical navigation — replacing the older NavigationView. It manages a genuine stack of screens, with a real back button and swipe-to-go-back gesture handled automatically.

struct Task: Identifiable, Hashable { let id = UUID() let title: String } struct TaskListView: View { let tasks: [Task] var body: some View { NavigationStack { List(tasks) { task in NavigationLink(task.title, value: task) } .navigationTitle("Tasks") .navigationDestination(for: Task.self) { task in TaskDetailView(task: task) } } } }
The Real, Deliberate Split
Rather than embedding a destination view directly inside every NavigationLink, NavigationLink(value:) only declares what real data was tapped — .navigationDestination(for:destination:), attached once at the NavigationStack's own level, decides what view to actually show for that type of data. Any real Task, tapped from anywhere in this stack, routes through the same one real destination.
A Real Requirement
navigationDestination(for:) matches by real, exact type, using Hashable conformance to track navigation state — Task needs to be both Identifiable (for List/ForEach, Chapter 7) and Hashable (for this).

Modal Presentation: .sheet()

A modal sheet is a real, genuinely different pattern from pushing a screen — it floats above the current screen rather than replacing it, and is dismissed by swiping down or an explicit action, not a back button.

struct TaskListView: View { @State private var isShowingNewTask = false var body: some View { NavigationStack { List { /* ... */ } .toolbar { Button("Add") { isShowingNewTask = true } } .sheet(isPresented: $isShowingNewTask) { NewTaskView() } } } }

A second, real form, .sheet(item:), presents a sheet whenever a bound optional value becomes non-nil — genuinely useful when the sheet's own content depends directly on which specific item was tapped, rather than a plain on/off flag.

ToolReal BehaviorUse It For
NavigationLinkPushes a new screen onto the stack; a real back button/swipe returnsDrilling into a hierarchy — a list to a detail screen
.sheet()Presents a screen modally above the current one; dismissed by the userA focused, temporary task — adding an item, a form, a confirmation

Passing Data Back

NavigationLink/navigationDestination naturally hands data forward, into the destination. Sending data back — like a newly created task returning from NewTaskView — reuses this course's own Chapter 6 tools directly: a @Binding passed into the sheet's own view, or an @Observable model shared between both screens.

Hands-On Exercises

Exercise 1

Build a NavigationStack containing a List of five sample Task items (using this chapter's own struct), each a NavigationLink(value:), with a .navigationDestination(for: Task.self) showing a simple detail screen with the task's own title.

📄 View solution
Exercise 2

Add an @State private var isShowingSheet = false and a toolbar Button to the view from Exercise 1, presenting a simple modal sheet with .sheet(isPresented:) when tapped.

📄 View solution
Exercise 3

Explain, in your own words, why routing every Task through one shared .navigationDestination(for: Task.self) — rather than embedding a destination view inside each individual NavigationLink — is a genuinely better real design for a list with many rows.

📄 View solution

Chapter 8 Quick Reference

  • NavigationStack (iOS 16+) — the real, current hierarchical navigation container, replacing NavigationView
  • NavigationLink(value:) declares what data was tapped; .navigationDestination(for:destination:), attached once, decides what view to show for that type
  • .sheet(isPresented:)/.sheet(item:) present a screen modally above the current one, dismissed by the user, not a back button
  • navigationDestination(for:) requires the routed type to be Hashable
  • Sending data back from a pushed or presented screen reuses @Binding or an @Observable shared model (Chapter 6)