If I have some state that holds an array of items, I can create a scoped store that operates on a single item of that list using the forEach function on the reducer. And when I change the single item, it also updates inside of the list. Great!
struct Model: Equatable, Identifiable {
let id: Int
}
struct AppState: Equatable {
var models: IdentifiedArrayOf<Model> = .init()
}
enum AppAction: Equatable {
case model(id: Int, action: ModelAction)
}
enum ModelAction: Equatable {}
let modelReducer = Reducer<Model, ModelAction, AppEnvironment> { state, action, _ in
return .none
}
let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
Reducer { state, action, _ in
return .none
},
modelReducer(
state: \.models,
action: /AppAction.model(id:action:),
environment: { _ in AppEnvironment() }
)
)
But what if I want the single item state to have a bit more.. state? Not just the bare Model. For example:
struct ModelState: Equatable {
var model: Model
var somethingElse: Bool = false
}
let modelReducer = Reducer<ModelState, ModelAction, AppEnvironment> { state, action, _ in
return .none
}
I can't use a forEach function anymore, since IdentifiedArrayOf<Model> doesn't map to ModelState. So I guess I need to use the pullback function to manually transform this? How can I do that in a way so that I can still create a scoped store where modifying the single Model also gets updated in the AppState's array?
1 Like
Have you looked at the list-based navigation demos? They have the idea of a "selection," which could be used to introduce additional state:
Interesting, thanks for the pointer. After looking at that example I came up with the following code:
import ComposableArchitecture
import Foundation
struct Model: Identifiable, Equatable {
let id: Int
var title: String
}
struct AppState: Equatable {
var models: IdentifiedArrayOf<Model> = .init()
var selection: Identified<Model.ID, ModelState>?
}
struct ModelState: Equatable {
var model: Model
var somethingElse: Bool = false
}
enum AppAction: Equatable {
case model(ModelAction)
case selectModel(Model.ID)
}
enum ModelAction: Equatable {
case changeTitle(String)
}
struct AppEnvironment {}
let modelReducer = Reducer<ModelState, ModelAction, AppEnvironment> { state, action, _ in
switch action {
case .changeTitle(let title):
state.model.title = title
return .none
}
}
let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
Reducer { state, action, _ in
switch action {
case .model:
return .none
case .selectModel(let id):
if let model = state.models[id: id] {
state.selection = Identified(ModelState(model: model), id: id)
}
return .none
}
},
modelReducer
.pullback(
state: \Identified.value,
action: .self,
environment: { $0 }
)
.optional
.pullback(
state: \AppState.selection,
action: /AppAction.model,
environment: { $0 }
)
)
.debug()
But sadly it doesn't work as I expect: when I change the single model in a scoped store, it's not also changed in the list.
let store = Store<AppState, AppAction>(
initialState: AppState(models: .init([Model(id: 1, title: "Kevin")]), selection: nil),
reducer: appReducer,
environment: AppEnvironment()
)
let viewStore = ViewStore(store)
viewStore.send(.selectModel(1))
let scoped = store.scope(
state: { $0.selection },
action: { AppAction.model($0) }
)
let scopedViewStore = ViewStore(scoped)
scopedViewStore.send(.changeTitle("Stephen"))
Obviously I'm missing a crucial bit here 
Yeah unfortunately you need to do some kind of coordination. In the above example we update the row's count when the row is "deselected":