Ever since I started using TCA, WithViewStore
always felt weird to me – it felt like I was infecting store logic into my view body
properties, which I always like to keep clean and pure (viewStore.binding
gives me the same sensation, but there's no getting rid of that ). Paired with its weird behaviour with certain types of views like GeometryReader
, I've diverted from using it, and have instead stuck to using a store
property and an @ObservedObject var viewStore
. This has been reinforced by the fact that isowords also prefers to use this method over WithViewStore
.
However, the two properties + custom initialisation, although nothing which can't be solved with a Sourcery template, feels cumbersome and repetitive. That's why I created the simple @WrappedStore
property wrapper to get rid of this boilerplate. It looks like this:
@propertyWrapper
public struct WrappedStore<State, Action>: DynamicProperty {
public let wrappedValue: Store<State, Action>
@ObservedObject public var projectedValue: ViewStore<State, Action>
public init(wrappedValue store: Store<State, Action>) where State: Equatable {
self.init(wrappedValue: store, removeDuplicates: ==)
}
public init(wrappedValue store: Store<State, Action>, removeDuplicates isDuplicate: @escaping (State, State) -> Bool) {
self.wrappedValue = store
self.projectedValue = ViewStore(store, removeDuplicates: isDuplicate)
}
}
Now, views can look like this:
struct ContentView: View {
@WrappedStore var store: Store<AppState, AppAction>
var body: some View {
ChildView(store: store.scope(state: \.childState, action: AppAction.childAction))
}
}
struct ChildView: View {
@WrappedStore var store: Store<ChildState, ChildAction>
var body: some View {
VStack {
TextField("String", text: $store.binding(get: \.text, send: ChildAction.setText))
Button("Increment Counter: \($store.counterState)") {
$store.send(.increment)
}
}
}
}
I wish the Store could be the projectedValue, and ViewStore the wrappedValue (since it's used more), but then I wouldn't be able to have the store:
initialiser – I'm open to solutions though. And of course, the name WrappedStore is open to bikeshedding.
Would love to hear what @stephencelis and @mbrandonw think about this!