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())")
}
}
}
I think the issue is that you use
self.store
inside of theWithViewStore
closure. You are supposed to use theviewStore
instead that you get in the closure argument.