Using Functional Binding to Observe in SwiftUI
Story
This week, my colleague asked me a question: how to observe user selection
behaviors on SwiftUI's Picker?
This is a question came from real bussiness. Thus I think it worth to take me time to solve it.
The example code is as shown below and my colleague wanted to observe user's
behaviors on selecting candidates of the Picker.
swiftimport SwiftUIlet labels = ["One", "Two", "Three", "Four"]struct ContentView: View {var data = Array(labels.enumerated())@Statevar selection: Int = 0var body: some View {Picker("Picker", selection: $selection) {ForEach(data, id: \.offset) { (_, label) inText(label)}}.pickerStyle(.inline)}}
Analysis
However, the meaning of "observe" varies over contexts:
- It can mean the time that user gets its finger down to the picker.
- It can mean the time that user gets its finger up from the picker.
- It can mean the time that the code changes the value of $selection.
Each of these leads to different solutions.
Since SwiftUI controls can adopt style modifiers which change the appearance and behavior of a control, to achieve the goal that observing the first two kinds of user behaviors that I mentioned above may need deep customizations over the control itself.
But if you just want to observe the time that the code changes the value of
$selection, you must try functional Binding.
"Wait! There is
onChange(of:, perform:)modifier, why should I use what you called functionalBinding?"
OK. We just have touched the key to my colleague's question: the timing of
onChange(of:, perform:) is difficult to predict and control.
In my colleague's code, he triggers network request and user behavior
observation with onChange(of: perform). But the callback of network
request always comes about 30ms earlier than the callback of user behavior
observation. This phenomenon is caused by SwiftUI's evaluation order. You
can control the order by arranging the onChange(of:, perform:) modifiers
on SwiftUI's View hierarchy in a fine-grained order.
But we are engineering -- we cannot make the position to put modifiers to be
coupled with SwiftUI's View evalution order!
Solution
To solve this problem, I suggest my colleague to wrap $selection with a
Binding by initializing with the following initializer:
swiftpublic struct Binding<Value> {public init(get: @escaping () -> Value,set: @escaping (Value, Transaction) -> Void)}
This is what I mentioned functional Binding.
Here is a use case of the initializer by combining the demo code I showned at the beginning of the post:
swiftstruct ContentView: View {// ...var selectionBinding: Binding<Int> {Binding(get: {$selection.wrappedValue},set: { (newValue, tnx) in// call `.transaction` to make use of the incomming// `tnx : Transaction` object.$selection.transaction(tnx).wrappedValue = newValueobserveSelectionChange()})}func observeSelectionChange() {// do what you want to do}var body: some View {Picker("Picker", selection: selectionBinding) {ForEach(data, id: \.offset) { (_, label) inText(label)}}.pickerStyle(.inline)}}
In observeSelectionChange function you can organize your logics on user
behavior observation.
Conclusion
Here is a couple of reasons why I recommend this way of user behavior observation:
SwiftUI is driven by value changes. This means that value changes are ubiquitous in a running SwiftUI program to offer a lot of observation points.
Bindingis more powerful than what you thought. It supports projection with key-paths and collection subscripts which enables developers to conducts partial value changes to a control or aView. This means that for Apple shipped SwiftUI controls and well-designed third-party SwiftUI controls you can observe all value changes conducted with theseBindings by wrapping a functionalBinding.Bindingcan observe all kinds of changes that can drive SwiftUI to updateViewcontents. In contrast,onChange(of:, perform:)requires developers to observe overEquatablevalues. But there are types that are not ofEquatablebut also able to drive SwiftUI to updateViewcontents. Here is one example: closures.In a functional
Bindingyou can choose whether to observe before or after the value change and control the order of actions that triggerred by the value change.