Pass State information back to previous State in navigation stack

Hey all, I've been trying to work with TCA NavigationStackStore and I think I may be doing something wrong. I have a root view that can navigate to 2 different child views. It looks like this:

struct RootDomain: Reducer {
    struct Path: Reducer {
        enum State {
            case child1(Child1Domain.State)
            case child2(Child2Domain.State)
        }
        enum Action {
            case child1(Child1Domain.Action)
            case child2(Child2Domain.Action)
        }
        var body: some ReducerOf<Self> {
            Scope(state: /State.child1, action: /Action.child1) {
                Child1Domain()
            }
            
            Scope(state: /State.child2, action: /Action.child2) {
                Child2Domain()
            }
        }
    }
    
    struct State {
        var path = StackState<Path.State>()
    }
    enum Action {
        case path(StackAction<Path.State, Path.Action>)
    }
    
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            return .none
        }
        .forEach(\.path, action: /Action.path) {
          Path()
        }
    }
}

struct RootView: View {
    
    var store: StoreOf<RootDomain> = .init(initialState: .init(), reducer: { RootDomain() })
    
    var body: some View {
        NavigationStackStore(self.store.scope(state: \.path, action: { .path($0) })) {
            NavigationLink(state: RootDomain.Path.State.child1(.init())) {
                Text("Root")
            }
        } destination: { store in
            switch store {
            case .child1:
                CaseLet(
                    /RootDomain.Path.State.child1,
                     action: RootDomain.Path.Action.child1,
                     then: Child1View.init(store:)
                )
            case .child2:
                CaseLet(
                    /RootDomain.Path.State.child2,
                     action: RootDomain.Path.Action.child2,
                     then: Child2View.init(store:)
                )
            }
        }

    }
}

When clicking the NavigationLink it will push Child1View onto the stack. Child1 looks like this:

struct Child1Domain: Reducer {
    struct State: Equatable {
        var child2State = Child2Domain.State()
    }
    enum Action: Equatable {}
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            return .none
        }
    }
}

struct Child1View: View {
    var store: StoreOf<Child1Domain>
    
    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            NavigationLink(state: RootDomain.Path.State.child2(viewStore.child2State)) {
                Text("Child 1")
            }
            Text("Child 2 was tapped: \(viewStore.child2State.wasTapped.description)")
        }
    }
}

When the link is clicked in Child1View, Child2 is then pushed onto the stack. I want to keep track of the state changes in Child2 from Child1. Here is Child2:

struct Child2Domain: Reducer {
    @Dependency(\.dismiss) var dismiss
    
    struct State: Equatable {
        var wasTapped = false
    }
    enum Action: Equatable {
        case buttonTapped
    }
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .buttonTapped:
                state.wasTapped = true
                return .run { _ in await self.dismiss() }
            }
        }
    }
}

struct Child2View: View {
    var store: StoreOf<Child2Domain>
    
    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            Button("Child 2") {
                viewStore.send(.buttonTapped)
            }
        }
    }
}

How can I get access to Child2 state and actions from Child1 if the path is located in the root, and navigation is done by NavigationLinks?

Hi @wheelius, this is something that is just generally difficult with navigation stacks, even in vanilla SwiftUI. One of the main points of navigation stacks is for each feature to be fully decoupled from each other, and for only the root to have knowledge about each feature. And because of that the integration glue between features usually happens in the root, not in each child feature.

In fact, right now the child2State you hold in Child1 is not related at all to the state that is held back at the root navigation path:

struct Child1Domain: Reducer {
  struct State: Equatable {
    var child2State = Child2Domain.State()
  }
  …

It is completely untethered. Any changes to it will not be reflected in the feature pushed onto the stack, and vice-versa.

What you are trying to accomplish is very easy in tree-based navigation, because then the parent feature directly navigates to the child feature. But also the APIs in SwiftUI for tree-based navigation are a lot more buggy.

If you want to keep with navigation stacks and truly want the Child1 feature to have access to state from Child2, then one approach is just to have the parent domain implement that glue code. You can simply have the parent domain pass along state from the Child2 to Child1.

This can be complicated to do. You will need to analyze the path collection of features, find any instances of Child2 state in it, and then find the Child1 that precedes it in order to pass along some state. It's tough, but it is possible.

That's what I was afraid of... I recall tree-based navigation is not so good for cyclic navigation (content -> profile -> content -> profile), whereas stack based does this well. If I glue them at the root it does seem to work. It looks like this:

case let .path(.element(id, action: .child2(.buttonTapped))):
    guard case let .child2(childState) = state.path[id: id] else { return .none }
    
    let parent = state.path.compactMap { (pathState) -> Child1Domain.State? in
        guard case let .child1(parentState) = pathState else { return nil }
        
        return parentState.id == childState.parentID ? parentState : nil
    }.first
    
    guard var parent = parent else { return .none }
    
    parent.child2State = childState
    let parentIDIndex = state.path.ids.firstIndex { $0 == id } ?? 0
    
    guard parentIDIndex > 0 else { return .none }
    
    let parentID = state.path.ids[parentIDIndex - 1]
    state.path[id: parentID] = .child1(parent)
    
    return .none

It is a bit of work (and potentially buggy when scaling?). Wondering if there is a way to auto do this? I gave the parent and child id's to tie themselves to each other like so:

struct Child1Domain: Reducer {
    struct State: Equatable, Identifiable {
        var id: Int
        var child2State: Child2Domain.State
        
        init() {
            let tempID = Int.random(in: 0...1000)
            self.id = tempID
            self.child2State = Child2Domain.State(parentID: tempID)
        }
    }
...
}