How to make bindings work with ViewState? #769
Replies: 3 comments 8 replies
-
@tgrapperon Thanks for starting the discussion! It is actually totally possible for
Here's an example in the Tic Tac Toe demo, which heavily adopts view state: https://github.com/pointfreeco/swift-composable-architecture/compare/tic-tac-toe-bindings |
Beta Was this translation helpful? Give feedback.
-
I've realized that we can define a convenient The immediate advantage is that it works out of the box when using a
One inconvenient is that is introduces a degree of freedom where we can mismatch I don't know how to name this method, nor if the public extension ViewStore {
func binding<ParentState, Value>(
_ parentKeyPath: WritableKeyPath<ParentState, BindableState<Value>>,
as keyPath: KeyPath<State, Value>
) -> Binding<Value>
where Action: BindableAction, Action.State == ParentState, Value: Equatable {
binding(
get: { $0[keyPath: keyPath] },
send: { .binding(.set(parentKeyPath, $0)) }
)
}
} Using this method, one can instantiate a binding using viewStore.binding(\.$property, as:\.viewStateProperty) // vs. viewStore.binding(\.$property) without a ViewState with When switching from What are you thinking about this? Do you see a better way to name/arrange things? |
Beta Was this translation helpful? Give feedback.
-
Always in the spirit of making TCA bindings work in foreign domains, I would like to mention the public extension Binding {
func bimap<T>(get: @escaping (Value) -> T, set: @escaping (T) -> Value) -> Binding<T> {
.init {
get(wrappedValue)
} set: { newValue in
withTransaction(transaction) {
wrappedValue = set(newValue)
}
}
}
} This function is particularly convenient when the state's type is different from the binding's expected type. For example, when the state exposes a One classical approach is to define a computed property in For example, if viewStore.binding(\.$hexColor).bimap (get: fromHex, set: toHex) One drawback is that it's a tiny bit of logic happening inside the One can also define public extension Binding {
func emptyIfNil<T>() -> Binding<T> where Value == T?, T: RangeReplaceableCollection {
bimap(get: { $0 ?? T() }, set: { $0.isEmpty ? .none : $0 })
}
func nilIfEmpty() -> Binding<Value?> where Value: RangeReplaceableCollection {
bimap(get: { $0.isEmpty ? .none : $0 }, set: { $0 ?? Value() })
}
} which can be convenient to bridge This can probably be reformulated in a more general framework. Please let me know if you have any idea/comment! |
Beta Was this translation helpful? Give feedback.
-
In the current (and former) implementation of bindings in TCA, the
ViewStore
's state needs to be of the same type as the store'sState
[Edit: this is not exact - See @stephencelis answer below]. I'm opening this discussion to explore alternative ways to still use TCA bindings when using aViewState
.(That is some equatable type, derived from the store's
State
, and which is used to scope the store and instantiate a chiseledViewStore
that works on the bare minimum needed for the view).Right now, I'm seeing three possibilities:
Extracting a dedicated Store
One can derive an equatable writable sub-state from the state and scope the store. We need to pull back accordingly in the reducer (and the
binding()
call goes there instead)The inconvenient part is the boilerplate needed to install a read-write sub-state of the state.
Using something like #451 may help, though (I still need to open source the protocolized version)
We are also potentially using two
ViewStore
s at the same time in the same view, which could probably be confusing.Use the whole state with custom deduplication.
By using something like the
DeduplicationScope
exposed in #527, one can easily chisel aremoveDuplicate
function for anyState
by simply specifying a list of relevantKeyPath
. However, This is error-prone, as one can easily forget to update the list when adding new properties to the state. But theViewStore
work with the same state as theReducer
and bindings are working out of the box.Use something to “relocate” the
BindableState<Value>
One can use some additional property wrapper to store the link with the
State
for example. One can implement theViewStateBinding
property wrapper for example:We can use this property wrapper as:
The
ViewStore
's override allows them to work the same at call sites:viewStore.$string
.I'm not particularly happy with their declaration, though: we need to qualify the generics when we declare them, and we have to initialize their private storage with
self._string
. This is not very elegant, but this is a first draft, and it is very possible that better solutions exist. This can maybe be partially type-erased.We are de facto linking two
KeyPath
, so the machinery developed in #451 may be reused at some point.Are you seeing other alternatives, or some ways to improve the property wrapper solution?
Beta Was this translation helpful? Give feedback.
All reactions