ForEachStore and pullback

Sample code for the question below can be found at: https://github.com/CSCIX65G/TCAExample.git

I have a pair of states and actions that look like this:

// Top Level State
public struct AppState: Equatable {
    var selectedRow: Int? = .none
    var title: String = ""
    var navigations: [NavState] = ["Nav 1", "Nav 2", "Nav 3"]
        .enumerated()
        .map(NavState.init)
}

public extension AppState {
    enum Action: Equatable {
        case setSelectedRow(Int?)
        case navigationAction(index: Int, action: NavState.Action)
    }
}

// ForEachStore'd State
public struct NavState: Equatable {
    var index: Int
    var title: String
}

extension NavState: Identifiable {
    public var id: Int { index }
}

public extension NavState {
    enum Action: Equatable {
        case setTitle(String)
        case setSelectedRow(Int?)
    }
}

The lower level state has an action which can affect the upper level state. The lower state is displayed by nesting in a ForEachStore.

The reducer looks like this:

let reducer = Reducer<AppState, AppState.Action, Environment> { state, action, _ in
    switch action {
        case .navigationAction(index: _, action: let action):
            switch action {
                case .setTitle(let title):
                    state.title = title
                case .setSelectedRow(let row):
                    state.selectedRow = row
            }
            return .none
        case .setSelectedRow:
            state.selectedRow = .none
            return .none
    }
}.debug()

I don't like the switch-within-a-switch look there. I'd like the shape of the reducer to be: Reducer<AppState, NavState.Action, Environment> and to pull that back in a combine with a top-level reducer of type Reducer<AppState, AppState.Action, Environment>, but I can't because the navigationAction takes 2 args rather than 1. Is there something I am missing in CasePaths that would allow me to do such a pullback?

Just a note on this example. I work on an TabView-driven app (not SwiftUI or TCA yet) where one of the tabs is the account page. The user can login/logout/change profile in that tab and it affects global state used by everything else in the app.

We'e been discussing migrating the app and this is one of the places that we don't quite see how to implement yet. What motivated this example is that the user can have navigated down to a page that is two or three scope calls away from the main AppState with one of the scopes being inside a ForEachStore and we can't figure out how to pullback that context into the main state.

You can solve this by creating a new reducer for your NavState and then use the forEach reducer to pullback on an array of states.

(I renamed your reducer to appReducer)

let navReducer = Reducer<NavState, NavState.Action, Environment> { state, action, _ in
    switch action {
        case .setTitle(let title):
            state.title = title
            return .none
    }
}

let reducer = Reducer.combine(
    appReducer.pullback(state: \.self, action: /AppState.Action.self, environment: { $0 }),
    navReducer.forEach(state: \.navigations, action: /AppState.Action.navigationAction(index:action:), environment: { $0 })
)

I may be missing something. This seems to update the local NavState.title, but not the global AppState.title. The reason for wanting a signature of:

Reducer<AppState, NavState.Action, Environment>

was to have access to the AppState and the NavState.Action at the same time so that the NavState.Action could be used to update global state. It's precisely that signature that I'm wanting to pullback and can't get around. I'm left with doing the switch-within-a-switch that I show above.

I do this in a lot of similar circumstances where I need to modify global state in a scoped context, but i can't figure out how to do it with ForEach'd contexts. But like I said, I may be missing something in your solution.

Terms of Service

Privacy Policy

Cookie Policy