Combining IfLetStore and ForEachStore

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)
      }
    }
  })
}

Any help is much appreciated!

2 Likes

Hi @eimantas :wave:

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! :raised_hands:

3 Likes

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.

Hi Peter,

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.

Thanks! this worked like a charm!

P.S. It would be great not to have a separate action when no external state is needed.

P.S. It would be great not to have a separate action when no external state is needed.

I think, I don't fully understand this comment. Can you go into more detail?

Is the content of the ForEachStore not sending any actions and you'd like to pass a store that doesn't allow any action to be sent?

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:

enum ThingListAction {
  ...
  actionForASingleThing(Thing)
}

IfLetStore(
  store.scope(
    state: \.things
  ) { thingListStore in
  ForEachStore(thingListStore, action: ThingListAction.actionForASingleThing) { ... }
}

You can use Void if you'd like

enum ThingListAction {
  case loadThings
  case thing(id: Thing.ID, Void)
}
1 Like

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.

2 Likes

I'd be going for a single case enum as well.

Thanks for providing a solid justification :)

1 Like