Skip to main content

Overview

Stack-based navigation models navigation using collections of state. This allows deep-linking by constructing flat collections of data and supports complex, recursive navigation paths. Key tools:
  • StackState collection type
  • StackAction type
  • .forEach(_:action:) reducer operator
  • Custom NavigationStack initializer

Basics

Integrating features into a navigation stack involves two main steps: integrating domains and constructing the NavigationStack view.

Step 1: Define Path Reducer

Create a reducer holding all features that can be pushed onto the stack:
The Path reducer is identical to the Destination reducer used in tree-based navigation with enums.

Step 2: Add StackState and StackAction

Hold navigation stack state and actions in the root feature:
StackActionOf is a typealias that simplifies the syntax for StackAction, which is generic over both state and action.

Step 3: Integrate with .forEach

Use .forEach to integrate path features with the parent:

Step 4: Build NavigationStack View

Use the custom NavigationStack initializer that takes a store binding:

Step 5: Handle Each Path Case

Use store.case to destructure each case and return the appropriate view:
Switching on store.case gives compile-time guarantees that you’ve handled all path cases.

Pushing Features onto the Stack

There are two primary ways to push features onto the stack: Use the custom NavigationLink initializer with full state:
When tapped, a StackAction.push(id:state:) action is sent, appending to the stack.
Drawback: This approach requires the view to access Path.State, meaning it must build all features in the path. This hurts modularity.

2. Using Button + Action (Modular)

Send an action from the child feature:
The root feature listens and appends to the path:
This approach maintains modularity since the child feature doesn’t need to know about Path.State.

Integration

Parent features have instant access to everything in the stack. Detect child actions by destructuring:
When destructuring StackAction.element(id:action:), you get both:
  • The action that happened
  • The ID of the element in the stack
StackState automatically manages IDs for every feature.

Dismissal

Dismiss features by mutating StackState:
Available methods:
  • popLast() - Remove last element
  • pop(from:) - Remove from specific ID
  • And more collection methods

Self-Dismissal from Child

Use @Dependency(\.dismiss) to allow children to dismiss themselves:
When dismiss() is called, a StackAction.popFrom(id:) action is sent to remove the feature from the stack.
Important: Never send actions after calling dismiss():
The feature’s state is no longer in the stack, causing runtime warnings and test failures.
SwiftUI’s @Environment(\.dismiss) and TCA’s @Dependency(\.dismiss) are different types:
  • SwiftUI’s: Use in views only
  • TCA’s: Use in reducers only

Testing

Using TCA’s tools makes testing navigation stacks straightforward. Non-exhaustive testing is especially useful.

Example: Testing Dismissal

Counter feature that dismisses when count ≥ 5:
Parent feature:

Test with IDs

Construct a test store with a counter already on the stack:
StackState automatically manages IDs. In tests, IDs are integers starting at 0 and incrementing for each feature pushed.
Send actions using ID 0:

Two Ways to Assert State Changes

Option 1: Using XCTModify
XCTModify takes an inout enum, extracts the case payload, lets you mutate it, and embeds it back. Option 2: Using Double Subscript
Simultaneously subscripts into an ID and a case of the enum.
Use XCTModify for many mutations, double subscript for simple ones.

Complete Test

Receiving Child Actions

To assert a specific child action is received, use subscript on the case key path:

Non-Exhaustive Test

Turn off exhaustivity for high-level assertions:
Non-exhaustive tests are more concise and resilient to changes you don’t care about.

StackState vs NavigationPath

SwiftUI provides NavigationPath, so why use StackState? Pros:
  • Type-erased list of any Hashable data
  • Maximal feature decoupling
Cons:
  • Limited API: only append, removeLast, and count
  • Cannot insert/remove from middle
  • Cannot iterate over elements
  • Hard to analyze stack contents

StackState

Pros:
  • Conforms to Collection, RandomAccessCollection, RangeReplaceableCollection
  • Access to many collection manipulation methods
  • Can iterate, insert, remove anywhere
  • Automatic stable identifier management
  • Data doesn’t need to be Hashable
Cons:
  • Fully statically typed (less flexible than type erasure)
StackState balances runtime flexibility with static, compile-time guarantees—perfect for TCA navigation.

UIKit

TCA provides NavigationStackController for state-driven UINavigationController:
Model your domains using StackState as described above, then use NavigationStackController to implement UIKit navigation.

Navigation Overview

Learn about navigation concepts and patterns

Tree-based Navigation

Learn about navigation with optionals and enums