import ComposableArchitecture import ComposablePresentation import SwiftUI struct NavigationStackExample: ReducerProtocol { struct State { var stack: IdentifiedArrayOf = [] } enum Action { case updatePath([Destination.State.ID]) case start case popToRoot case popTo(Destination.State.ID) case destination(_ id: Destination.State.ID, _ action: Destination.Action) } var body: some ReducerProtocol { Reduce { state, action in switch action { case .updatePath(let path): state.stack = state.stack.filter { destination in path.contains(destination.id) } return .none case .start: state.stack = [.init(id: "1")] return .none case .popTo(let id): if let index = state.stack.index(id: id) { state.stack = .init(uniqueElements: state.stack[state.stack.startIndex...index]) } return .none case .destination(let id, .push(let path)): state.stack.append(contentsOf: path.map { .init(id: "\(id).\($0)") }) return .none case .destination(_, .set(let path)): state.stack = .init(uniqueElements: path.map { id in state.stack[id: id] ?? .init(id: id) }) return .none case .destination(_, .pop): _ = state.stack.popLast() return .none case .popToRoot, .destination(_, .popToRoot): state.stack.removeAll() return .none case .destination(_, .shuffle): state.stack.shuffle() return .none case .destination(_, _): return .none } } .presentingForEach( state: \.stack, action: /Action.destination, onPresent: .init { id, state in // Start timer when destination is added to the stack. When multiple destinations are pushed onto the stack, only the view of the last one will receive `.onAppear` event (that starts the timer too). .task { .destination(id, .timer(.start)) } }, element: Destination.init ) } // MARK: - Child Reducers struct Destination: ReducerProtocol { struct State: Identifiable { var id: String var timer = TimerExample.State() } enum Action { case push([Destination.State.ID]) case set([Destination.State.ID]) case pop case popToRoot case shuffle case timer(TimerExample.Action) } var body: some ReducerProtocol { Scope(state: \.timer, action: /Action.timer) { TimerExample() } } } } struct NavigationStackExampleView: View { let store: StoreOf var body: some View { if #available(iOS 16, *) { VStack(spacing: 0) { NavigationStackWithStore(store.scope( state: { Array($0.stack.ids) }, action: NavigationStackExample.Action.updatePath )) { Button { ViewStore(store.stateless).send(.start) } label: { Text("Start") } .buttonStyle(.borderedProminent) .controlSize(.large) .navigationTitle("Root") .navigationDestination( forEach: store.scope(state: \.stack), action: NavigationStackExample.Action.destination, destination: DestinationView.init(store:) ) } Divider() WithViewStore(store, observe: \.stack.ids) { viewStore in ScrollView(.horizontal, showsIndicators: false) { HStack { Button { viewStore.send(.popToRoot) } label: { Text("Root") } ForEach(viewStore.state, id: \.self) { id in Text("→") Button { viewStore.send(.popTo(id)) } label: { Text(id) } } } } .padding() } } } else { Text("iOS ≥ 16 required") } } // MARK: - Child Views struct DestinationView: View { let store: StoreOf struct ViewState: Equatable { init(state: NavigationStackExample.Destination.State) { title = state.id } var title: String } var body: some View { WithViewStore(store, observe: ViewState.init) { viewStore in Form { Section { TimerExampleView(store: store.scope( state: \.timer, action: NavigationStackExample.Destination.Action.timer )) } header: { Text("Timer") } Section { Button(action: { viewStore.send(.push(["1"])) }) { Text("Push 1") } Button(action: { viewStore.send(.push(["2"])) }) { Text("Push 2") } Button(action: { viewStore.send(.push(["3"])) }) { Text("Push 3") } Button(action: { viewStore.send(.push(["1", "2", "3"])) }) { Text("Push 1→2→3") } Button(action: { viewStore.send(.push(["3", "2", "1"])) }) { Text("Push 3→2→1") } Button(action: { viewStore.send(.set(["1", "2", "3"])) }) { Text("Set 1→2→3") } Button(action: { viewStore.send(.set(["3", "2", "1"])) }) { Text("Set 3→2→1") } Button(action: { viewStore.send(.pop) }) { Text("Pop") } Button(action: { viewStore.send(.popToRoot) }) { Text("Pop to root") } Button(action: { viewStore.send(.shuffle) }) { Text("Shuffle") } } header: { Text("Stack navigation") } } .navigationTitle(viewStore.title) } } } } struct NavigationStackExample_Previews: PreviewProvider { static var previews: some View { NavigationStackExampleView(store: Store( initialState: NavigationStackExample.State(), reducer: NavigationStackExample() )) } }