How to reuse view and reducer with more than one model?

I'm just starting out with TCA but I am unsure how to create reusable views and reducers that can be used with different models.

As an example, I've created a reusable View that has a reducer factory that allows a different concrete type to be used for the view state as long as it conforms to a protocol:

EditableItemView.swift

protocol EditableItemConformable: Equatable, Identifiable {
    var id: UUID { get }
    var title: String { get set }
}

enum EditableItemAction: Equatable {
    case textFieldChanged(String)
}

func makeItemReducer<T>() -> Reducer<T, EditableItemAction, AppEnvironment> where T: EditableItemConformable {
    return Reducer { item, action, environment in
        switch action {
        case .textFieldChanged(let string):
            item.title = string
            return .none
        }
    }
}

struct EditableItemView<T>: View where T: EditableItemConformable {

    let store: Store<T, EditableItemAction>

    var body: some View {
        WithViewStore(store) { viewStore in
            TextField(
                viewStore.title,
                text: viewStore.binding(get: \.title, send: EditableItemAction.textFieldChanged)
            )
        }
    }
}

Given I have the following models:

Models.swift

struct ChildModel: EditableItemConformable {
    let id: UUID
    var title: String

    // other unrelated fields
    var isPopular: Bool
}

struct GroupModel: EditableItemConformable {
    let id: UUID
    var title: String
    var items: IdentifiedArrayOf<ChildModel> = []
}

Both of those can be used as the state for the EditableItemView.

Now putting this all together I have two screens that list items, the first screen is a list of "groups" that should have editable titles, the second screen is a list of "child" items that belong to the select group.

GroupsView.swift

enum GroupsViewAction: Equatable {
    case addGroup
    case delete(IndexSet)
    case move(IndexSet, Int)
    case editModeChanged(EditMode)
    case item(id: GroupModel.ID, action: GroupDetailsAction)
    case item2(id: GroupModel.ID, action: EditableItemAction)
}

struct GroupsViewState: Equatable {
    var items: IdentifiedArrayOf<GroupModel> = []
    var editMode: EditMode = .inactive
}

/// How can I use this reducer for a single view (EditableItemView) below?
let groupReducer: Reducer<GroupModel, EditableItemAction, AppEnvironment> = makeItemReducer()

let groupsViewReducer = Reducer<GroupsViewState, GroupsViewAction, AppEnvironment>.combine(
    groupDetailsReducer.forEach(
        state: \.items,
        action: /GroupsViewAction.item(id:action:),
        environment: { $0 }
    ),
    Reducer { state, action, environment in
        switch action {
        case .addGroup:
            let item = GroupModel(
                id: environment.uuid(),
                title: "Test",
                items: []
            )
            state.items.append(item)
            return .none

        case let .editModeChanged(editMode):
            state.editMode = editMode
            return .none

        case .item, .item2:
            return .none

        case let .delete(indexSet):
            state.items.remove(atOffsets: indexSet)
            return .none

        case let .move(source, destination):
            state.items.move(fromOffsets: source, toOffset: destination)
            return .none
        }
    }
)
.debugActions(actionFormat: .prettyPrint)

struct GroupsView: View {

    let store: Store<GroupsViewState, GroupsViewAction>

    var body: some View {

        WithViewStore(store) { viewStore in

            NavigationView {
                List {
                    ForEachStore(
                        self.store.scope(
                            state: \.items,
                            action: GroupsViewAction.item(id:action:)
                        )
                    ) { itemStore in
                        NavigationLink {
                            GroupDetailsView(store: itemStore)
                        } label: {

                            /// Cannot convert value of type 'Store<GroupModel, GroupDetailsAction>' to expected argument type 'Store<GroupModel, EditableItemAction>'
                            /// Need to be able to scoped version of `itemStore`for each item that works with the `EditableItemAction`, how
                            /// can I achieve this?
                            EditableItemView<GroupModel>(store: itemStore)
                                .padding()
                                .border(Color.red, width: 1)
                            Spacer()
                                .frame(width: 100)

//                            WithViewStore(itemStore) { viewStore in
//                                Text(viewStore.title)
//                            }
                        }
                    }
                    .onDelete { viewStore.send(.delete($0)) }
                    .onMove { viewStore.send(.move($0, $1)) }
                }
                .navigationTitle("Items")
                .toolbar {
                    HStack {
                        EditButton()
                        Button {
                            viewStore.send(.addGroup)
                        } label: {
                            Image(systemName: "plus")
                        }
                    }
                }
            }
            .environment(
              \.editMode,
               viewStore.binding(get: \.editMode, send: GroupsViewAction.editModeChanged)
            )
        }
    }
}

The detail screen looks like this:

GroupDetailsView.swift

enum GroupDetailsAction: Equatable {
    case addItem
    case delete(IndexSet)
    case move(IndexSet, Int)
    case item(id: ChildModel.ID, action: EditableItemAction)
}

let childReducer: Reducer<ChildModel, EditableItemAction, AppEnvironment> = makeItemReducer()

let groupDetailsReducer = Reducer<GroupModel, GroupDetailsAction, AppEnvironment>.combine(
    childReducer.forEach(
        state: \.items,
        action: /GroupDetailsAction.item(id:action:),
        environment: { $0 }
    ),
    Reducer { state, action, environment in
        switch action {
        case .addItem:
            let item = ChildModel(
                id: environment.uuid(),
                title: "Test",
                isPopular: false
            )
            state.items.append(item)
            return .none

        case .item:
            return .none

        case let .delete(indexSet):
            state.items.remove(atOffsets: indexSet)
            return .none

        case let .move(source, destination):
            state.items.move(fromOffsets: source, toOffset: destination)
            return .none
        }
    }
)
.debugActions(actionFormat: .prettyPrint)

struct GroupDetailsView: View {

    let store: Store<GroupModel, GroupDetailsAction>

    var body: some View {

        WithViewStore(store) { viewStore in

            List {
                ForEachStore(
                    self.store.scope(
                        state: \.items,
                        action: GroupDetailsAction.item(id:action:)
                    )
                ) { groupDetails in
                    HStack {
                        EditableItemView<ChildModel>(store: groupDetails)
                        WithViewStore(groupDetails) { viewStore in
                            if viewStore.isPopular {
                                Text("Popular!")
                            }
                        }
                    }
                }
                .onDelete { viewStore.send(.delete($0)) }
                .onMove { viewStore.send(.move($0, $1)) }
            }
            .toolbar {
                HStack {
                    EditButton()
                    Button {
                        viewStore.send(.addItem)
                    } label: {
                        Image(systemName: "plus")
                    }
                }
            }
            .navigationTitle(viewStore.title)
        }
    }
}

If you want to try building this you'll need an entry point:

main.swift

import ComposableArchitecture
import SwiftUI

@main
struct testApp: App {
    var body: some Scene {
        WindowGroup {
            GroupsView(
                store: Store(
                    initialState: GroupsViewState(items: .mock),
                    reducer: groupsViewReducer,
                    environment: AppEnvironment(
                        uuid: UUID.init
                    )
                )
            )
        }
    }
}

struct AppEnvironment {
    var uuid: () -> UUID
}

extension IdentifiedArray where ID == GroupModel.ID, Element == GroupModel {
  static let mock: Self = [
    GroupModel(
      id: UUID(uuidString: "DEADBEEF-DEAD-BEEF-DEAD-BEEDDEADBEEF")!,
      title: "Group 1",
      items: .mock
    ),
    GroupModel(
      id: UUID(uuidString: "CAFEBEEF-CAFE-BEEF-CAFE-BEEFCAFEBEEF")!,
      title: "Group 2",
      items: .mock
    ),
    GroupModel(
      id: UUID(uuidString: "D00DCAFE-D00D-CAFE-D00D-CAFED00DCAFE")!,
      title: "Group 3",
      items: .mock
    ),
  ]
}

extension IdentifiedArray where ID == ChildModel.ID, Element == ChildModel {
  static let mock: Self = [
    ChildModel(
      id: UUID(uuidString: "2A543D97-844A-4BAA-B648-86B6478D475A")!,
      title: "Item 1",
      isPopular: false
    ),
    ChildModel(
      id: UUID(uuidString: "9C4436EF-6D5A-4CE0-9321-08FA8C7DD0D7")!,
      title: "Item 2",
      isPopular: true
    ),
    ChildModel(
      id: UUID(uuidString: "FC604136-38E0-4306-BFF2-3CCBE8FB5E34")!,
      title: "Item 3",
      isPopular: false
    ),
  ]
}

The issue I'm having is this code in the GroupsView.swift:

                    ForEachStore(
                        self.store.scope(
                            state: \.items,
                            action: GroupsViewAction.item(id:action:)
                        )
                    ) { itemStore in
                        NavigationLink {
                            GroupDetailsView(store: itemStore)
                        } label: {

                            /// Cannot convert value of type 'Store<GroupModel, GroupDetailsAction>' to expected argument type 'Store<GroupModel, EditableItemAction>'
                            /// Need to be able to scoped version of `itemStore`for each item that works with the `EditableItemAction`, how
                            /// can I achieve this?
                            EditableItemView<GroupModel>(store: itemStore)
                                .padding()
                                .border(Color.red, width: 1)
                            Spacer()
                                .frame(width: 100)

It's unclear to me how to scope the already scoped itemStore so that it uses the GroupsViewAction.item2(id:action:) which is needed because the action is EditableItemAction which is needed to perform the editing action on the EditableItemView's reducer.

Can anyone advise how I can work with a scoped array using ForEach with two different actions?

I'm wondering if I'm approaching this all wrong but it feels like I should be able to have shared views that have their own reducers that can work with different concrete types as long as they conform to a protocol.

Gist: ReusableReducerAndView.swift · GitHub

For anyone interested, the solution was to create an intermediate action and then re-scope the store from the ForEach viewStore. See: How to reuse view and reducer with more than one model? · Discussion #1010 · pointfreeco/swift-composable-architecture · GitHub