I have a state that contains an optional IdentifiableArrayOf<Thing>.
struct ThingListState: Equatable {
var things: IdentifiableArrayOf<Thing>?
}
I load those asynchronously when view appears. And afterwards I'd love to list them:
var body: some View {
IfLetStore(store.scope(\.things), then: { listViewStore in
// how do I use ForEachStore (or plain ForEach)
// so that I can list things?
}, else: {
WithViewStore(store) { vs in
ProgressView().onAppear {
vs.send(.loadThings)
}
}
})
}
The listViewStore above is Store<IdentifiableArrayOf<Thing>,ThingListAction> and I have hard time figuring out how I can use it to list Things and send actions for each individual thing. I get a weird warning if I try to use listViewStore.scope(state: \.self, action: ListViewAction(id:thingAction:):
public enum ThingListAction {
...
case thing(id: String, action: ThingAction)
...
}
public enum ThingAction { case dummy }
...
var body: some View {
IfLetStore(store.scope(\.things), then: { listViewStore in
ForEachStore(listViewStore.scope(state: \.self, action: ThingListAction.thing(id:action:)) { _ in
// ^-- this is where I get error: Cannot convert value of type
// 'WritableKeyPath<_, _>' to expected argument type
// '(IdentifiedArrayOf<Thing>) -> IdentifiedArray<String, _>'
// (aka '(IdentifiedArray<String, Thing>) -> IdentifiedArray<String, _>')
// among other many errors about type inference.
}
}, else: {
WithViewStore(store) { vs in
ProgressView().onAppear {
vs.send(.loadThings)
}
}
})
}
I checked your provided code and got it to compile:
struct Thing: Equatable, Identifiable {
let id: UUID
}
struct ThingListState: Equatable {
var things: IdentifiedArrayOf<Thing>?
}
enum ThingListAction {
case loadThings
case thing(id: Thing.ID, action: ThingAction)
}
public enum ThingAction { case dummy }
import SwiftUI
struct SomeView: View {
let store: Store<ThingListState, ThingListAction>
var body: some View {
IfLetStore(
store.scope(state: \.things, action: ThingListAction.thing),
then: { listViewStore in
ForEachStore(listViewStore) { itemStore in
Text("A thing")
}
},
else: {
WithViewStore(store) { vs in
ProgressView().onAppear {
vs.send(.loadThings)
}
}
}
)
}
}
Instead of specifying ThingListAction.thing(id:action:), specify the case ThingListAction.thing. Any enum case can be used as a function (UnnamedAssociatedValueTuple) -> Root.
You can scope the ThingListAction to (ID, ThingAction) as part of the IfLetStore scoping and pass the unwrapped store directly into the ForEachStore.
ThingListAction specified case thing(id: String, action: ThingAction) instead of the type-safe case thing(id: Thing.ID, action: ThingAction). Any identifiable object has an associated type ID that you can use.
Hope this helps. If anything remains unclear, feel free to ask more questions!
Using a computed property here seems like a very bad idea. A new id will be returned each time this property is accessed. Ids are supposed to be persistent identifiers.
I absolutely agree, thanks for addressing this! I just introduced the computed property to get compiling code, this should not be done in production code.
I'll edit my answer so that the id needs to be passed on init.
I think it's a bit superfluous to create a separate enum for scoped store (in my case that's an enum with a single action for tapping the Thing in ThingListState.things. It'd be great to have a ForEachStore overload that doesn't require you to scope the action, but map it back to the parent. Something along the lines of:
Sending void as an Action is a clever trick as Void is equatable and can only ever take one value ().
Personally, I would prefer expressiveness here, but I get your point.
Adding another enum does not hurt and allows to add additional actions which can be performed by the View representing 'Thing' at a later point. As an added benefit, viewStore.send(.thingAction) is easier to read than viewStore.send(()).
If you really don't want to add an additional, one-case enum to your codebase, you could consider adding a struct ThingAction: Equatable {} which would be similar to the void approach, but requires initializing an empty struct to perform an action. However, I don't see a huge benefit over introducing an enum.