swift-composable-architecture
                                
                                 swift-composable-architecture copied to clipboard
                                
                                    swift-composable-architecture copied to clipboard
                            
                            
                            
                        Add ViewState binding support regarding to discusstion#842
This is an implementation of the idea of BindableViewState  proposed by @stephencelis in discussion #842.
Hey @yuanhang, thanks for exploring this! We haven't had time to look over it yet, but hopefully in the next few days.
Hey @yuanhang, thanks for exploring this! We haven't had time to look over it yet, but hopefully in the next few days.
Thanks, no rush. Your work has really helped me a lot and I'm happy to make my little contribution (if I can).
This is my first PR for TCA, and I haven't found a guide to contribute, so I'm happy to change anything that doesn't fit the TCA code requirements.
This is possibly redundant. @tgrapperon in one of the Binding/ViewStore discussions earlier have mentioned his solution
public extension ViewStore {
  func binding<ParentState, Value>(
    _ parentKeyPath: WritableKeyPath<ParentState, BindableState<Value>>,
    as keyPath: KeyPath<ViewState, Value>
  ) -> Binding<Value>
  where ViewAction: BindableAction, ViewAction.State == ParentState, Value: Equatable {
    binding(
      get: { $0[keyPath: keyPath] },
      send: { .binding(.set(parentKeyPath, $0)) }
    )
  }
}
Which allows to elegantly bind to BindableState from a ViewStore, if needed, like:
PlanPicker(
   selected: self.viewStore.binding(\.$selectedPlanID, as: \.selectedPlanID),
)
Where \.$selectedPlanID is BindableState property, while \.selectedPlanID is a key path on a ViewState.
This is possibly redundant. @tgrapperon in one of the Binding/ViewStore discussions earlier have mentioned his solution
public extension ViewStore { func binding<ParentState, Value>( _ parentKeyPath: WritableKeyPath<ParentState, BindableState<Value>>, as keyPath: KeyPath<ViewState, Value> ) -> Binding<Value> where ViewAction: BindableAction, ViewAction.State == ParentState, Value: Equatable { binding( get: { $0[keyPath: keyPath] }, send: { .binding(.set(parentKeyPath, $0)) } ) } }Which allows to elegantly bind to BindableState from a ViewStore, if needed, like:
PlanPicker( selected: self.viewStore.binding(\.$selectedPlanID, as: \.selectedPlanID), )Where
\.$selectedPlanIDis BindableState property, while\.selectedPlanIDis a key path on a ViewState.
Thank you for your reply. To avoid misunderstanding:
- Would you post the link to the @tgrapperon discussion you mentioned so I can go through the context?
- It would be great if you could give a complete example (a unit test/gist maybe) that I can run to better understand the solution.
@yuanhang I think that @andreyz referred to #769.
@yuanhang I think that @andreyz referred to #769.
Thank you for your reply!
I went and read the whole discussion, however, I need to (embarrassingly) admit that I failed to understand how this works. I tried to write this code in the TCA source code, but there seem to be some other parts missing to make the compilation pass. It would be nice to have an example that runs.
In this specific case, you have
struct State {
  @BindableState var text0: String // Added `0` or `1` suffix to make it clearer
}
struct MyView: View {
  …
  struct ViewState: Equatable {
    var text1: String // No @BindableState required here
	init(_ state: State) {
      self.text1 = state.text0
    }
  }
  var body: some View {
    WithViewStore(store, observe: ViewState.init) { viewStore
      TextField("Name", text: viewStore.binding(\.$text0, as: \.text1))
    }
  }
}
It merely reads \.text1, but writes on \.$text0, while hoping that you made the correct connection in your ViewState.init. The nice thing is that switching from observe: { $0 } to observe: ViewState.init only requires additive changes in your view (appending ,as: \.text1). It can be implemented as a third party, out of TCA. It also works as expected with case .binding(\.$text0): pattern matching in reducers. Of course, in real-life scenarios, you don't have the 0 or 1 suffixes, so you write viewStore.binding(\.$text, as: \.text).
But this is one solution among many others (including those described in #769. Until now, none is 100% satisfactory. I don't think it's possible to introduce seamless workaround without formalizing additional constraints (like introducing a protocol or a new property wrapper of some sort)
Hi @yuanhang! Thanks again for taking the time to submit this after following previous discussions. One of the reasons we held off on bringing this particular solution into the library was we weren't entirely happy with the ergonomics. @mbrandonw and I have been experimenting with alternatives for awhile, but @tgrapperon recently cracked the code in #1790, which we think offers the nicest and most streamlined way of using @BindingState (previously @BindableState) with view state.
I'm going to close this out for now, but would love to hear your thoughts on the solution we came to!