How to create a 'one-way' binding?

I'm trying to work out the best way to handle a one-way binding with SwiftUI.


There's lots of state that isn't two way. For example: if you have some view state that is derived from some deeper model state, it doesn't make sense for this to be settable within your view. Ideally it would be read-only.


But then you might say, "well, just pass this state directly – you don't need a binding!".


OK, but then how do you perform the equivalent of Binding's `animation(_:)` or `transaction(_:)` modifiers to perform some fancy explicit animation when your state updates?


Maybe then you might say, "well, pass in a Publisher of that state instead and use `withAnimation(_:_:)` in an `onReceive(_:_:)` block".


Right, but then how do I do the equivalent of State's '$myState.substate' trick to pass a derived publisher down from my original publisher? Am I not going to create a ton of overhead in the render tree creating tons of mapped publishers from my source publisher each time my view tree updates?

Post not yet marked as solved Up vote post of Luxo Down vote post of Luxo
4.7k views

Replies

Bindings are intended for both reading and writing. There's no rule that says you must write to the value.


Generally speaking, if you're passing value types, you have a single @State somewhere, and everywhere else you either pass a copy of the wrapped value or you pass a binding (if you want the sub-type to have access to any changes made to the underlying value). If you're passing around an object type then you use an @ObservedObject or @EnvironmentObject, both of which support the $ syntax to obtain @Binding wrappers for contained value type variables. In essence, $myState.substate will yield a Binding<Substate> whenever myState is an @State, @BInding, @EnvironmentObject, or @ObservedObject property.


Another option is to have your child view contain a duplicated @State or @ObservedObject value that is a copy of the original. Doing this with @State has some side-effects, though: SwiftUI uses these types to determine if the view tree needs to be re-evaluated. If A creates B, and B has its own @State property, then the view tree below B will only be replaced if B's @State variable is modified. SwiftUI sees that the body of B only used the content of one @State property, so it will not update its contents unless that @State instance is modified. For an example, see Why is my child SwiftUI view not updating?


In your case, I would just use @Binding and not write to it. That way you let SwiftUI know what the state relationship is, so your view updates will behave appropriately; any view accessing the @Binding will be redrawn/updated when the underlying @State changes.

Bindings are intended for both reading and writing. There's no rule that says you must write to the value.

Thanks this is really useful. It seems it may be the only choice in the short-term.


In the long-term though, I hope we're offered a better solution as it leaves something to be desired.


For example, say you create an ObservableObject which has some read-only property you'd like to derive Bindings from to pass down the view-tree:


You can't create a WriteableKeyPath to it as it's read-only, which means you can't create a Binding as the Binding subscript requires a WriteableKeyPath. Therefore, the workaround is to create an intermediate computed property which returns the desired state in a 'get' clause, and has some kind of assertion failure in the 'set' clause to warn that this is a binding that shouldn't be written to.


I think this will work, but it seems to require a lot of boilerplate for something that should be simple.


Ideally, they'd be a read-only counterpart to Binding that could be derived from any read-only KeyPath. That's why I initially thought maybe I had missed something and perhaps a Publisher was supposed to fulfil this role – but it seems a Publisher isn't party to the same kind of treatment a Binding receives in terms of SwiftUI performance optimisations etc. as it's mapped and passed down the view-tree and so this won't work.

I think you may be overcomplicating things. Descendent views that depend on the state variable in question will be torn down and recreated with the new value when the variable owned by the ancestor view changes—no bindings needed. I made this mistake for a while too. If you want a change in state to be animated just wrap it in withAnimation{}.


If this is still unclear, do you want to post some sample code that illustrates what you're trying to accomplish and why the above approach doesn't meet your needs?

That’s actually the first route I took.


I have a publisher that produces a large state object (conforming to hashable), so at the root of my view tree I have an onReceive block that passes in the state to a top-level view as a straight struct – right into its initialiser.


Where I became stuck was what happens when I want one part of that state update to initiate an explicit animation of one kind, and another part of that state to initiate no animation or some other kind of animation. It’s clear that bindings have the animation modifier. I had hoped that I could use the withAnimation block. But with state passed in as a raw struct to a view’s initialiser, where would that call even go? Ideally they’d be a way to do this.


Intuitively , passing in a deep state object as a raw struct, and then passing down partial sub-states into sub-views seems a reasonable strategy with SwiftUI’s diffing behaviour. But I’m struggling to see the hook for specialising animations for partial sections of the view-tree/sub-state. I’ve made use of some onAppear /onDisappear hacks to modify some local @state which then forces an animation – but I don’t feel it’s particularly elegant.

Thinking some more, maybe just using the animation modifier after passing the SubView some substate is equivalent:


SubView(subState: state.subState).animation(MyAnimation())


That would be elegant. I'll have to investigate.