Model Typed Navigation Routes in SwiftUI

Model Typed Navigation Routes in SwiftUI

A library can open a book, enter a chapter, or show help. Represent those destinations as typed values instead of magic strings, and make the current route sequence inspectable as a path.

You will learn: Define a Hashable route enum; append and clear a homogeneous route path; register destinations in the stack hierarchy; separate route identity from full model storage.

Prerequisites: the earlier SwiftUI Foundations lessons and Swift values/closures. These projects use the installed iOS SDK and local in-memory fixtures. They do not modify personal data or contact a server. The screenshot shows the initial screen before the interaction sequence.

Library routes, Open book 42, Open help
Actual iPhone 18 Pro simulator capture, iOS 27.0; local apple-vision verification.

Describe destinations as data

LibraryRoute has book(Int), chapter(bookID:number:) and help cases. Associated values carry the small identifiers needed by the destination. Hashable conformance lets the values participate in value-based navigation.

enum LibraryRoute: Hashable {
    case book(Int)
    case chapter(bookID: Int, number: Int)
    case help
}
@State private var path: [LibraryRoute] = []

A route is not a database record. Passing compact stable identifiers avoids treating a large copied model as navigation identity. A production destination must resolve those IDs and handle a missing or inaccessible record; this fixture displays them directly to isolate navigation.

Register how each route becomes a view

NavigationStack(path: $path) binds the presented route sequence. navigationDestination(for:) maps a LibraryRoute to a destination. Keep that registration in the stack’s active hierarchy rather than hiding it inside a lazily created row.

.navigationDestination(for: LibraryRoute.self) { route in
    switch route {
    case .book(let id): /* book screen */ Text("Book ID: \(id)")
    case .chapter(let id, let number): Text("Book \(id), chapter \(number)")
    case .help: Text("Choose a book to start reading.")
    }
}

This is a shortened destination mapping; the complete built file below includes the actual controls. Exhaustive switching makes a newly added route a deliberate implementation task rather than an unhandled string branch.

Append a destination or return to the root

Open book 42 appends .book(42). Read chapter appends .chapter(bookID:42, number:1), so the path contains two values. The chapter’s Back to library button removes all values and reveals the root.

path.append(.book(42))
path.append(.chapter(bookID: 42, number: 1))
path.removeAll()

These statements illustrate the sequence; they do not all run at once in the app. System back navigation removes the last destination, while removeAll expresses a different action: return to root. Validate preconditions before manually removing a last element from a potentially empty array.

Keep deep links and restoration explicit

An explicit path is a foundation for programmatic navigation, but this example is not a complete deep-link parser or restoration system. External URLs must be parsed, validated and authorized before producing routes. A typed Int can still identify an invalid or forbidden record.

Use a homogeneous [Route] when one enum naturally describes the flow. NavigationPath offers a type-erased option for mixed Hashable values, with different tradeoffs. Do not introduce it merely because a simple enum path looks too short.

Build the complete example

Download the LibraryRoutes Xcode project. Open LibraryRoutes.xcodeproj, select an installed compatible iPhone simulator, and run. The app entry point already presents ContentView. The complete ContentView.swift below is the exact built source; earlier snippets are explanatory excerpts.

import SwiftUI

struct ContentView: View {
@State private var path: [LibraryRoute] = []
 var body: some View {
NavigationStack(path: $path) {
 VStack(spacing: 24) {
  Button("Open book 42") { path.append(.book(42)) }.accessibilityIdentifier("openBook")
  NavigationLink("Open help", value: LibraryRoute.help)
 }.navigationTitle("Library routes")
 .navigationDestination(for: LibraryRoute.self) { route in
  switch route {
  case .book(let id):
   VStack(spacing: 20) {
    Text("Book ID: \(id)").font(.title)
    Button("Read chapter") { path.append(.chapter(bookID: id, number: 1)) }.accessibilityIdentifier("readChapter")
   }.navigationTitle("Book")
  case .chapter(let bookID, let number):
   VStack(spacing: 20) {
    Text("Book \(bookID), chapter \(number)")
    Button("Back to library") { path.removeAll() }.accessibilityIdentifier("libraryRoot")
   }.navigationTitle("Chapter")
  case .help: Text("Choose a book to start reading.").navigationTitle("Help")
  }
 }
}
 }
}

enum LibraryRoute: Hashable { case book(Int), chapter(bookID: Int, number: Int), help }

#Preview { ContentView() }

Accessibility identifiers in the file provide stable names for local automation; they are not visible menu labels. #Preview is an editor convenience. The following screenshots come from the launched app after actual automated control interactions.

Push a typed book route

Press Open book 42. The destination shows Book ID: 42 and Read chapter. The route carries 42 into the mapping; the title alone would not prove that value survived.

Book ID: 42, Read chapter
Actual iPhone 18 Pro simulator capture, iOS 27.0; local native-accessibility verification.

Append a second route

Press Read chapter. The next destination displays Book 42, chapter 1. This is a second pushed route, so a normal back action could return to the book.

Book 42, chapter 1, Back to library
Actual iPhone 18 Pro simulator capture, iOS 27.0; local native-accessibility verification.

Clear the path to return home

Press Back to library. Clearing the path removes both destinations and restores the root choices. The automation checks the visible root after that programmatic mutation.

Library routes, Open help
Actual iPhone 18 Pro simulator capture, iOS 27.0; local native-accessibility verification.

Check a failure at the right boundary

If your output differs, compare the current input values and the relevant action before changing project settings. Save the complete source, confirm the selected scheme and destination, and inspect the first build diagnostic if compilation fails. A successful build establishes valid source; the action-result checks establish the particular UI transitions shown here.

Practice variations below are proposed changes, not additional captured states. Keep input validation, state ownership and presentation decisions explicit as you extend the project.

Try a new case and explain the result

Add an author(Int) route and a root control for author 7. Explain what must change besides the enum.

Worked answer

Add a switch branch that produces the author destination and a value-based link or append action carrying 7. The compiler requires the switch to remain exhaustive. If the author comes from an external URL, validate and resolve the identifier separately.

Check your understanding

Choose the explanation that follows from the worked example.

What does path.removeAll() express?
Choose one answer. You can try again.

Answer and explanation

Return to the root by removing every pushed route.

Correct. Trace the owned data and the operation used by the example.

What to take forward

You navigated with typed destination data, inspected a two-step path and cleared it deliberately. Next, present a separate editing task with sheets and alerts.

Apple documentation.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Present Sheets and Confirmation Alerts in SwiftUI →