-
Hey everyone 👋 I’ve been going through the TCA Bindings documentation and trying to apply it in my project. The approach works great with flat state, but I’m running into issues when the state gets more nested. I made a simplified example to illustrate the issue. Here’s my setup: import ComposableArchitecture
import IdentifiedCollections
import Foundation
@Reducer
struct Feature {
@ObservableState
struct State: Equatable {
var response: Response?
}
struct Item: Equatable, Identifiable {
var id = UUID()
var isOn: Bool?
}
struct Response: Equatable {
var items: IdentifiedArrayOf<Item>?
}
enum Action: Equatable, BindableAction {
case binding(BindingAction<State>)
case onAppear
case onToggle(id: UUID, isOn: Bool)
}
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
return .none
case .onAppear:
state.response = Response(
items: IdentifiedArrayOf(
uniqueElements:
[
Item(isOn: true),
Item(isOn: false),
Item(isOn: true)
]
)
)
return .none
case .onToggle(let id, let isOn):
state.response?.items?[id: id]?.isOn = isOn
return .none
}
}
}
}
import SwiftUI
struct TogglesView: View {
@Bindable var store: StoreOf<Feature>
var body: some View {
Text("")
}
private func bindind(for id: UUID) -> Binding<Bool>? {
$store.response?.items?[id: id]?.isOn
}
private func simpleBinding(for id: UUID) -> Binding<Bool>? {
guard let isOn = store.response?.items?[id: id]?.isOn else {
return nil
}
return Binding<Bool>(
get: { isOn },
set: { newValue in
store.send(.onToggle(id: id, isOn: newValue))
}
)
}
} In my view, I tried to bind like this: private func bindind(for id: UUID) -> Binding<Bool>? {
$store.response?.items?[id: id]?.isOn // ❌ Compilation error
} But of course, this fails with:
Instead, I fallback to a manual binding with .onToggle action: private func simpleBinding(for id: UUID) -> Binding<Bool>? {
guard let isOn = store.response?.items?[id: id]?.isOn else { return nil }
return Binding(
get: { isOn },
set: { store.send(.onToggle(id: id, isOn: $0)) }
)
} This works, but I’m wondering: is this the recommended approach for nested state like this in TCA? Or is there a more elegant way to get bindings in such cases using TCA’s helpers? I realize the nested optionals might look like a smell, but they actually reflect business logic — certain parts of the state are truly optional depending on the business logic flow. Would love to hear how others tackle this. Thanks! |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
Hi @atimca, this is a problem that goes beyond TCA. You would have the exact same problem in vanilla SwiftUI. The moment you cross the boundary of an optional you lose writability in key paths. The only thing you can do is define custom properties on your types to bridge to the non-optional world in order to maintain writability. These new properties can either be ad hoc or can be generic. For example, if you had a subscript like so: extension Optional {
subscript(coalesce value: Wrapped) -> Wrapped {
get { self ?? value }
set { self = newValue }
}
} …then you would be able to do: $store
.response[coalesce: Response()]
.items[coalesce: []][id: id][coalesce: Item()]
.isOn Sure it's ugly, but that's just how things are until Swift has a better storing for chaining key paths through optionals. And although you say these optionals are necessary, I would take a hard look at them and 100% make sure that is the case. |
Beta Was this translation helpful? Give feedback.
Hi @atimca, this is a problem that goes beyond TCA. You would have the exact same problem in vanilla SwiftUI. The moment you cross the boundary of an optional you lose writability in key paths. The only thing you can do is define custom properties on your types to bridge to the non-optional world in order to maintain writability.
These new properties can either be ad hoc or can be generic. For example, if you had a subscript like so:
…then you would be able to do:
S…