SwiftUI binding: A very simple trick

Binding is a fundamental concept for SWIFTUI. According to Apple documentation:
Use a binding to create a two-way connection between a view and its underlying model. For example, you can create a binding between a Toggle and a Bool property of a State. Interacting with the toggle control changes the value of the Bool, and mutating the value of the Bool causes the toggle to update its presented state.
You can get a binding from a State by accessing its binding property. You can also use the $ prefix operator with any property of a State to create a binding.
One of the initializers of Binding is:

 init(get: @escaping () -> Value, set: @escaping (Value) -> Void)  
In this post, I will show you 2 very useful utilisations of this initializer, that will make your life easier.

Binding to an array:

Prior to beta4 of Xcode 11, it was fairly simple to bind a list of controls to elements of an array. However, in beta5, if you are using this, you will get a warning:
subscript(_:)’ is deprecated: See Release Notes for a migration path
And the release notes of beta5 give a fairly complicated way to avoid this deprecation. I will not detail here on this migration path, but you can read about it by searching the section starting with “Several extensions to the Binding structure are removed. (51624798)” in the above link.
However the get/set initialiser of Binding gives a much simpler way to overcome this issue.
Here is how to achieve this:
a. I define a @State array of names

 @State var items:[String] = [ "John", "Bill", "Mary", "Pete", "Jane", "Tom",  
  "Caroline", "Mindy"]  
 
b. I loop on the array of names and create a TextField for each name in the array. I create a Binding for the TextField elements, where get is pointing to the correct position in the state array, while set is setting the new value in the state array

 List{   
    ForEach(items.indices, id:\.self ){ idx in    
      TextField("", text: Binding(    
           get: {  return self.items[idx] },   
           set: { (newValue) in  return self.items[idx] = newValue }))   
     }  
 }  

Extending the functionality of a control:

Take a look at Stepper control. It has a number of initialisers, but in terms of data interactions, they even get a binding for a two way link between data and the control or a pair of onIncrement/onDecrement callbacks. But not both at the same time.
However the set/get initialiser for Binding can actually provide both, and this is how:
a. I declare a @State of type Int
@State var numberOfDays: Int = 23
b. I create a Stepper control and instead of simply binding it with the state, using $, I use a set/get initialised Binding for my Stepper. In this way, the Stepper gets the value of the @State, the @State gets updated when stepper +/- buttons are used, but on top of this, I have an entry point in the set method to call additional methods for increment/decrement. Comparing the newValue with the state variable, gives the right information if the stepper action was an increment or a decrement. In this case I am printing this information, however more complicated logic can be used instead.

 Stepper(value: Binding(  
      get: { return self.numberOfDays },   
      set: { (newValue) in   
   if self.numberOfDays<newValue{  
        print("increase")    
   }   
   else{  
        print("decrease")   
   }   
   self.numberOfDays = newValue   
   }),   
   in: 0...10000,   
   label: {   
             Text("Number of days: \(numberOfDays)")  
   })  
   .padding()  
Using the set/get initialiser for Binding is a very useful tool to get both a binding but also to react on the value change event for your control. There are obviously a large amount of utilisations and each developer can use this technique in various creative ways, to achieve the desired behaviour and to enhance the SwiftUI controls with functionality that they lack at the moment.

Comments

Popular posts from this blog

SwiftUI: Custom navigation

SwiftUI: handling images in Dark theme

SwiftUI - interacting with UIKit - part1: map view