Overview
Tree-based navigation uses optional and enum state to model navigation. This style allows you to deep-link into any state by constructing deeply nested state and handing it to SwiftUI. Key tools:@PresentsmacroPresentationActiontype.ifLet(_:action:destination:)reducer operator
Basics
Integrating features for navigation involves two steps: integrating domains and integrating views.Step 1: Integrate Domains
Add child state and actions to the parent using@Presents and PresentationAction:
The
addItem state is optional. Non-nil means presented, nil means dismissed..ifLet and add an action to populate child state:
Step 2: Integrate Views
Pass a binding of a store to SwiftUI’s view modifiers:Use SwiftUI’s
@Bindable to produce a binding to a store, then scope it using .scope(state:action:).sheet(item:)popover(item:)fullScreenCover(item:)navigationDestination(item:)- And more
Enum State
Modeling multiple destinations with multiple optionals creates invalid states:Solution: Use an Enum
Model multiple destinations as a single enum:Implementation
- Define a Destination Reducer
@Reducer macro on an enum to auto-generate the full reducer:
- Hold a Single Optional State
- Integrate with
.ifLet
- Present Features by Setting Enum Cases
- Scope Views to Specific Cases
API Unification
One of tree-based navigation’s best features is API unification. Regardless of navigation type (drill-down, sheet, alert, etc.):- Domain integration uses the single
.ifLetoperator - View integration provides a store focused on presentation state/action
Backwards Compatibility
For iOS <16, macOS <13, tvOS <16, watchOS <9, use thisNavigationLink helper:
Integration
Parent features get instant access to everything in child features. Detect child actions by destructuring:Dismissal
Dismiss by nil-ing out the state:Self-Dismissal from Child
Use the@Dependency(\.dismiss) to allow children to dismiss themselves:
SwiftUI’s
@Environment(\.dismiss) and TCA’s @Dependency(\.dismiss) are different types with different purposes:- SwiftUI’s: Use in views only
- TCA’s: Use in reducers only
Testing
Properly modeled navigation makes testing straightforward. Non-exhaustive testing is especially useful for navigation.Example: Testing Dismissal
Counter feature that dismisses when count ≥ 5:Exhaustive Test
Non-Exhaustive Test
Turn off exhaustivity for high-level assertions:Testing with Enum State
When using enum destinations, chain into the specific case:Related
Navigation Overview
Learn about navigation concepts and patterns
Stack-based Navigation
Learn about navigation with collections