I am pretty new to TCA and have been rebuilding one of my apps while following along with the A tour of the Composable Architecture 1.0 series.
I've been able to completely reproduce one of the main features of my existing using the TCA but I'm fairly certain what I'm doing is incorrect. The overall apps purpose is to assist in the editing of a nested data structure. For simplicity, consider the following data structures:
// MODELS
struct A: Equatable {
var bees: [B]
}
struct B: Identifiable, Equatable {
var id: UUID
var cees: [C]
}
struct C: Equatable {
var someField = ""
}
As you can see, the A struct has an array of B structs which further have an array of C structs. I have three views, composed together, where the main state associated with the view corresponds to one of the data structures. That is, my AView uses a store (AFeature) with access to an A struct AND more notably has an array of BFeature.States for integration with a ForEachStore. The BView uses a store with access to a B struct and has an arrya of CFeature.States for integration with a ForEachStore. See the diagram below for a visualization of the above.
My issue is that with this set up, in my reducers, I end up writing somewhat redundant statements to mutate both the state's raw struct and the IdentifiedArrayOf<someFeature.State>. I've added delete examples in the diagram above. An add example follows:
case .addCee:
let newCee = C()
state.b.cees.append(newCee)
state.cees.append(CFeature.State(id: UUID(), c: newCee))
return .none
This seems a little strange to me and isn't a scenario covered in ForEachStore documentation and I was unable to find a good example elsewhere.
Issue
This doesn't actually work, when appending a new B to A's array and appending a new BFeature.State, I can edit the B within the B view, but those edits never make it back to the B that the A struct is holding, it only changes the BFeature.State. This problem occurs at every level of the hierarchy. I'm not actually mutating A except for adding or deleting Bees from it.
Questions
-
The A data structure won't be very large, should I consider modeling this with one state, instead of three? For example, there would probably never be more than 10 Bees and 10 Cees per B. So the data structure is not just not large, it's likely to be small.
-
How should I conceptually think about problems like this, are there any resources you can point me to where this type of modelling is discussed?
-
I'm open to any ideas here about this any advice, recommendations, tips, tricks are greatly appreciated. If it is important, I'm eventually going to persist this data on disk and or to a backend store so any forward thinking recommendations wtih that in mind would be great. What ya got?
References
- This post and the case study it references might be what I want, will need to fully review these
Minimal Compilable Example
import SwiftUI
import ComposableArchitecture
// MODELS
struct A: Equatable {
var bees: [B]
}
struct B: Identifiable, Equatable {
var id: UUID
var cees: [C]
}
struct C: Equatable {
var someField = ""
}
// VIEWS
struct AView: View {
let store: StoreOf<AFeature>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
List {
ForEachStore(self.store.scope(state: \.bees, action: \.bees)) { bStore in
BView(store: bStore)
}
}
Button("Add") {
viewStore.send(.addBee)
}
}
}
}
struct BView: View {
let store: StoreOf<BFeature>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
List {
ForEachStore(self.store.scope(state: \.cees, action: \.cees)) { ceeStore in
CView(store: ceeStore)
}
}
.frame(height: calculateListHeight(count: viewStore.cees.count))
Button("Add") {
viewStore.send(.addCee)
}
}
}
func calculateListHeight(count: Int) -> CGFloat {
return CGFloat(count) * 100
}
}
struct CView: View {
let store: StoreOf<CFeature>
var body: some View {
HStack {
Text("someField")
}
}
}
// REDUCERS
@Reducer
struct AFeature {
struct State: Equatable {
var a: A
var bees: IdentifiedArrayOf<BFeature.State> = []
}
enum Action {
case addBee
case bees(IdentifiedActionOf<BFeature>)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .bees:
return .none
case .addBee:
let newBee = B(id: UUID(), cees: [])
state.bees.append(BFeature.State(id: UUID(), b: newBee))
state.a.bees.append(newBee)
return .none
}
}
.forEach(\.bees, action: \.bees) {
BFeature()
}
}
}
@Reducer
struct BFeature {
struct State: Identifiable, Equatable {
var id: UUID
var b: B
var cees: IdentifiedArrayOf<CFeature.State> = []
}
enum Action {
case addCee
case cees(IdentifiedActionOf<CFeature>)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .cees:
return .none
case .addCee:
let newCee = C()
state.b.cees.append(newCee)
state.cees.append(CFeature.State(id: UUID(), c: newCee))
return .none
}
}
.forEach(\.cees, action: \.cees) {
CFeature()
}
}
}
@Reducer
struct CFeature {
struct State: Identifiable, Equatable {
var id: UUID
var c: C
}
enum Action {
}
var body: some ReducerOf<Self> {
Reduce { state, action in
return .none
}
}
}
#Preview {
AView(
store: Store(initialState: AFeature.State(
a: A(bees: []),
bees: [])) {
AFeature()
._printChanges()
}
)
}