Richer input and navigation

Navigate with NavigationStack

Present destinations from stable data and keep navigation state explicit when the app needs to inspect or restore it.

8 minute lesson

~~~

NavigationView appears in older SwiftUI tutorials, but Apple deprecated it. New code should use NavigationStack for one-column navigation. The change is not cosmetic: NavigationView hid its navigation state, so you could not inspect it, restore it, or drive it from code without hacks. NavigationStack makes that state an explicit value your app can own.

The simplest stack owns its navigation internally. You describe destinations as data, and tell the stack how to turn each data type into a screen:

NavigationStack {
  List(notes) { note in
    NavigationLink(note.title, value: note.id)
  }
  .navigationDestination(for: UUID.self) { id in
    NoteDetail(id: id)
  }
}

Tapping a link pushes its value onto the navigation path. The navigationDestination(for:) modifier matches values by type: every UUID pushed on this stack becomes a NoteDetail. This split between “what was selected” and “how it is presented” is what makes the API composable — links stay tiny, and destinations live in one place.

When the app needs programmatic navigation, bind the path to state:

struct NotesRoot: View {
  @State private var path: [UUID] = []

  var body: some View {
    NavigationStack(path: $path) {
      List(notes) { note in
        NavigationLink(note.title, value: note.id)
      }
      .navigationDestination(for: UUID.self) { id in
        NoteDetail(id: id)
      }
    }
  }
}

Now navigation is just data. Appending an id pushes a screen: path.append(note.id). Setting path = [] pops back to the root. A deep link handler can build the whole array in one assignment and the stack renders the full hierarchy, back button included. If you need to push values of different types onto the same stack, use NavigationPath instead of a typed array — it accepts any Hashable value.

Push stable, lightweight identifiers instead of whole model objects. A common mistake is pushing a full Note struct into the path. It works at first, but the pushed copy goes stale the moment the model changes, and restoring state or handling a deep link means reconstructing entire objects instead of writing an id. Let the destination view resolve the current model from the identifier it receives.

One pointer for later: on iPad and Mac, apps with a sidebar and a detail area should reach for NavigationSplitView instead. It manages the columns, and its detail column can still contain a NavigationStack of its own.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →