Skip to main content
State-driven navigation is a powerful concept in application development. The Composable Architecture provides tools to model your domains concisely and drive navigation from state.

What is navigation?

For the purposes of this documentation, we use the following definition:
Navigation is a change of mode in the application.
Each form of navigation—drill-downs, sheets, popovers, covers, alerts, dialogs, and more—represents a “change of mode” in the application. More specifically:
A change of mode is when some piece of state goes from not existing to existing, or vice-versa.
When a piece of state switches from not existing to existing, that represents navigation and a change of mode. When it switches back to not existing, it represents undoing the navigation.

Two forms of navigation

Navigation broadly falls into 2 main categories:

Tree-based

Use optionals and enums to model navigation in a tree-like structure

Stack-based

Use flat collections to model navigation stacks
Nearly all applications will use a combination of both styles, but it’s important to know their strengths and weaknesses.

Tree-based navigation

Tree-based navigation uses Swift’s Optional type to represent existence or non-existence of state. When multiple states of navigation are nested, they form a tree-like structure.

Basic example

@Reducer
struct InventoryFeature {
  @ObservableState
  struct State {
    @Presents var detailItem: DetailItemFeature.State?
    // ...
  }
  // ...
}

Deep linking

With tree-based navigation, deep-linking is simply constructing deeply nested state:
InventoryView(
  store: Store(
    initialState: InventoryFeature.State(
      detailItem: DetailItemFeature.State(      // Drill-down
        editItem: EditItemFeature.State(        // Open sheet
          alert: AlertState {                   // Show alert
            TextState("This item is invalid.")
          }
        )
      )
    )
  ) {
    InventoryFeature()
  }
)

Pros and cons

  • Concise modeling: Statically describe all valid navigation paths
  • Finite paths: Impossible to restore invalid navigation states
  • Self-contained modules: Features include their destinations, making previews fully functional
  • Easy integration testing: Write detailed tests proving feature interactions
  • API unification: Single style for all navigation types (sheets, drills, alerts, etc.)
  • Recursive paths are difficult: Complex navigation like movie → actors → actor → movie is hard to model
  • Feature coupling: Must compile all destination features together
  • Historical SwiftUI bugs: More susceptible to navigation bugs (though improved in iOS 16.4+)

Stack-based navigation

Stack-based navigation uses collections to model the presentation of features. An entire stack of features is represented by a collection of data.

Basic example

enum Path {
  case detail(DetailItemFeature.State)
  case edit(EditItemFeature.State)
  // ...
}

let path: [Path] = [
  .detail(DetailItemFeature.State(item: item)),
  .edit(EditItemFeature.State(item: item)),
  // ...
]

Complex navigation

Stack-based navigation easily handles recursive paths:
let path: [Path] = [
  .movie(/* ... */),
  .actors(/* ... */),
  .actor(/* ... */),
  .movies(/* ... */),
  .movie(/* ... */),  // Same feature, different data
]

Pros and cons

  • Handles complexity: Easily manages complex and recursive navigation paths
  • Decoupled features: Each feature can be in its own module with no interdependencies
  • Fewer bugs: NavigationStack API is more stable than tree-based alternatives
  • Non-sensical states possible: Can express invalid navigation orders
  • Limited modularity: Features in isolation are mostly inert in previews
  • Integration testing harder: Difficult to test feature interactions when fully decoupled
  • Only for drill-downs: Doesn’t address sheets, popovers, alerts, etc.

Choosing an approach

Most real-world applications use both approaches:
1

Start with stack-based for main flow

Use NavigationStack and stack-based navigation for your app’s primary navigation flow.
2

Use tree-based for modals and auxiliary UI

Within each feature in the stack, use tree-based navigation for sheets, popovers, alerts, and other modal presentations.
3

Consider your priorities

  • Need finite, well-defined paths? → Tree-based
  • Need recursive or complex paths? → Stack-based
  • Need easy integration testing? → Tree-based
  • Need fully decoupled features? → Stack-based
The Composable Architecture provides several tools for implementing navigation:

Tree-based tools

  • @Presents macro for optional state
  • PresentationAction for handling child actions
  • ifLet reducer operator for integration
  • Navigation view modifiers for sheets, popovers, alerts, etc.

Stack-based tools

  • StackState for modeling stack collections
  • StackAction for stack-related actions
  • forEach reducer operator for integration
  • NavigationStack initializer tuned for TCA

Next steps

Tree-based Navigation

Learn the details of implementing tree-based navigation

Stack-based Navigation

Learn the details of implementing stack-based navigation

Dismissal

Learn how to dismiss features from child domains

Testing Navigation

Learn how to test navigation flows

Example: Hybrid approach

Here’s how a typical app might combine both approaches:
@Reducer
struct AppFeature {
  @ObservableState
  struct State {
    // Stack-based navigation for main flow
    var path = StackState<Path.State>()
  }
  
  @Reducer
  enum Path {
    case home(HomeFeature)
    case profile(ProfileFeature)
  }
}

@Reducer
struct HomeFeature {
  @ObservableState
  struct State {
    // Tree-based navigation for modals
    @Presents var settings: SettingsFeature.State?
    @Presents var alert: AlertState<Action.Alert>?
  }
}
This hybrid approach gives you the best of both worlds: a flexible main navigation flow with well-defined modal presentations.

Build docs developers (and LLMs) love