Using Child SwitchStore Views within a Parent's ForEachStore View

991 Views Asked by At

I'm facing an issue implementing The Swift Composable Architecture in which I have a list of IdentifiedArray rows within my AppState that holds RowState which holds an EnumRowState as part of it's state, to allow me to SwitchStore on it within a RowView that is rendered within a ForEachStore on the AppView.

The problem I'm running into is that within a child reducer called liveReducer, there is an Effect.timer that is updating every one second, and should only be causing the LiveView to re-render.

However what's happening is that the AddView is also getting re-rendered every time too! The reason I know this is because I've manually added a Text("\(Date())") within the AddView and I see the date changing every one second regardless of the fact that it's not related in anyway to a change in state.

I've added .debug() to the appReducer and I see in the logs that the rows part of the sate shows ...(1 unchanged) which sounds right, so why then is every single row being re-rendered on every single Effect.timer effect?

Thank you in advance!

Below is how I've implemented this:

P.S. I've used a technique as describe here to pullback reducers on an enum property: https://forums.swift.org/t/pullback-reducer-on-enum-property-switchstore/52628

Here is a video of the problem, in which you can see that once I've selected a name from the menu, ALL the views are getting updated, INCLUDING the navBarTitle!

https://youtube.com/shorts/rn_Yd57n1r8

struct AppState: Equatable {
  var rows: IdentifiedArray<UUID, RowState> = []
}

enum AppAction: Equatable {
  case row(id: UUID, action: RowAction)
}

public struct RowState: Equatable, Identifiable {
  public var enumRowState: EnumRowState
  public let id: UUID
}

public enum EnumRowState: Equatable {
  case add(AddState)
  case live(LiveState)
}

public enum RowAction: Equatable {
  case live(AddAction)
  case add(LiveAction)
}

public struct LiveState: Equatable {
  public var secondsElapsed = 0
}

enum LiveAction: Equatable {
  case onAppear
  case onDisappear
  case timerTicked
}

struct AppState: Equatable { }

enum AddAction: Equatable { }

public let liveReducer = Reducer<LiveState, LiveAction, LiveEnvironment>.init({
  state, action, environment in
  switch action {
  case .onAppear:
    return Effect
      .timer(
        id: state.baby.uid,
        every: 1,
        tolerance: .zero,
        on: environment.mainQueue)
      .map { _ in
        LiveAction.timerTicked
      })
 
 case .timerTicked:
    state.secondsElapsed += 1
    return .none
    
  case .onDisappear:
    return .cancel(id: state.baby.uid)
  }
})

public let addReducer = Reducer<AddState, AddAction, AddEnvironment>.init({
  state, action, environment in
  switch action {
  })
}

///
/// Intermediate reducers to pull back to an Enum State which will be used within the `SwitchStore`
///

public var intermediateAddReducer: Reducer<EnumRowState, RowAction, RowEnvironment> {
  return addReducer.pullback(
    state: /EnumRowState.add,
    action: /RowAction.add,
    environment: { ... }
  )
}

public var intermediateLiveReducer: Reducer<EnumRowState, RowAction, RowEnvironment > {
  return liveReducer.pullback(
    state: /EnumRowState.live,
    action: /RowAction.live,
    environment: { ...  }
  )
}

public let rowReducer: Reducer<RowState, RowAction, RowEnvironment> = .combine(
  intermediateAddReducer.pullback(
    state: \RowState.enumRowState,
    action: /RowAction.self,
    environment: { $0 }
  ),
  intermediateLiveReducer.pullback(
    state: \RowState.enumRowState,
    action: /RowAction.self,
    environment: { $0 }
  )
)

let appReducer: Reducer<AppState, AppAction, AppEnvironment> = .combine(
  rowReducer.forEach(
    state: \.rows,
    action: /AppAction.row(id:action:),
    environment: { ...   }
  ),
  .init({ state, action, environment in
    switch action {
    case AppAction.onAppear:
      state.rows = [
          RowState(id: UUID(), enumRowState: .add(AddState()))
          RowState(id: UUID(), enumRowState: .add(LiveState()))
      ]
      return .none
    default:
      return .none
    }
  })
)
.debug()

public struct AppView: View {
  
  let store: Store<AppState, AppAction>
  
  public var body: some View {
      WithViewStore(self.store) { viewStore in
        List {
            ForEachStore(
              self.store.scope(
                state: \AppState.rows,
                action: AppAction.row(id:action:))
             ) { rowViewStore in
               RowView(store: rowViewStore)
             }
          }        
    }
}

public struct RowView: View {
  public let store: Store<RowState, RowAction>
  
  public var body: some View {
      WithViewStore(self.store) { viewStore in
        SwitchStore(self.store.scope(state: \RowState.enumRowState)) {
          CaseLet(state: /EnumRowState.add, action: RowAction.add) { store in
            AddView(store: store)
          }
          CaseLet(state: /EnumRowState.live, action: RowAction.live) { store in
            LiveView(store: store)
          }
        }
      }
  }
}

struct LiveView: View {
  let store: Store<LiveState, LiveAction>

  var body: some View {
    WithViewStore(self.store) { viewStore in
      Text(viewStore.secondsElapsed)
    }
  }
}

struct AddView: View {
  let store: Store<AddState, AddAction>

  var body: some View {
    WithViewStore(self.store) { viewStore in
      // This is getting re-rendered every time the `liveReducer`'s  `secondsElapsed` state changes!?!
      Text("\(Date())")
    }
  }
}

1

There are 1 best solutions below

0
On

I think the issue is that you use self.store inside of the WithViewStore closure. You are supposed to use the viewStore instead that you get in the closure argument.