Navigation Basics
NavigationStack patterns with path reducers and programmatic navigation.
NavigationStack with Path Reducer
Basic Pattern
@Reducer
struct NavigationDemo {
@Reducer
enum Path {
case screenA(ScreenA)
case screenB(ScreenB)
case screenC(ScreenC)
}
@ObservableState
struct State: Equatable {
var path = StackState<Path.State>()
}
enum Action {
case path(StackActionOf<Path>)
case popToRoot
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .popToRoot:
state.path.removeAll()
return .none
case .path:
return .none
}
}
.forEach(\.path, action: \.path)
}
}View with store.case Pattern
struct NavigationDemoView: View {
@Bindable var store: StoreOf<NavigationDemo>
var body: some View {
NavigationStack(
path: $store.scope(state: \.path, action: \.path)
) {
RootView()
} destination: { store in
switch store.case {
case let .screenA(store):
ScreenAView(store: store)
case let .screenB(store):
ScreenBView(store: store)
case let .screenC(store):
ScreenCView(store: store)
}
}
}
}Navigation Actions
Pushing to Stack
case .view(.didTapNavigateToDetail):
state.path.append(.detail(Detail.State()))
return .none
case .view(.didTapNavigateToSettings):
state.path.append(.settings(Settings.State(id: state.selectedId)))
return .nonePopping from Stack
// Pop one screen
case .view(.didTapBack):
state.path.removeLast()
return .none
// Pop to root
case .view(.didTapPopToRoot):
state.path.removeAll()
return .none
// Pop to specific index
case .view(.didTapPopToFirst):
state.path.removeAll(after: 0)
return .noneProgrammatic Dismiss
Use @Dependency(\.dismiss) for child features to dismiss themselves:
@Reducer
struct DetailFeature {
@Dependency(\.dismiss) var dismiss
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .view(.didTapClose):
return .run { _ in
await self.dismiss()
}
case .view(.didSave):
return .concatenate(
.send(.delegate(.didSave)),
.run { _ in await self.dismiss() }
)
}
}
}
}Handling Child Actions
Responding to Delegate Actions
case .path(.element(id: _, action: .detail(.delegate(.didSave)))):
// Detail screen saved, pop it
state.path.removeLast()
return .send(.refreshData)
case .path(.element(id: _, action: .settings(.delegate(.didLogout)))):
// Settings logged out, pop to root
state.path.removeAll()
return .send(.delegate(.userDidLogout))Inspecting Navigation Stack
case .view(.didTapSave):
// Check if we're in a specific screen
guard state.path.last(where: { $0.is(\.detail) }) != nil else {
return .none
}
return .send(.path(.element(id: state.path.ids.last!, action: .detail(.save))))Enum Reducer Conformances
CRITICAL: When using @Reducer enum Path, add protocol conformances via extension:
@Reducer
struct NavigationDemo {
@Reducer
enum Path {
case screenA(ScreenA)
case screenB(ScreenB)
}
}
// Extension must be at file scope
extension NavigationDemo.Path: Equatable {}Best Practices
- Use
@Reducer enum Path- For type-safe navigation destinations - Use
StackState- For managing navigation stack state - Use
.forEach(\.path, action: \.path)- For path reducer composition - Use
@Dependency(\.dismiss)- For child features to dismiss themselves - Handle delegate actions - Pop stack or navigate based on child completion
- Extension conformances - Add
Equatablevia extension for enum reducers