Search code examples
iosswiftreactive-programmingreactive-swiftreactive-cocoa-5

Cyclic Bindings with ReactiveCocoa, How to bind one value to two input UI elements


In an iOS Project, i have to implement a view controller that allows to pick a float value using a slider and a text field at the same time. it should have the following behaviour:

  • whenever a number is typed in the text field, the slider should update it's value to the typed float number
  • whenever the slider is dragged it should update the text field with the slider value

Current ideas is as follows:

I have two propertys in the view model, the actual value and the value text. Whenever the value is updated i update the text within the view model. Both the text field and the slider change only the value. But then i have a kind of 'cyclic' dependency: text field updates the view models value, which updates the view models text, which is bound to the textfield where we just type the value. This leads to buggy behaviour of the text field. I tried to work around with the isFirstResponder property, that works better, but does not seem best practise, plus it's not as intended. Is there a way bind the enabledProperty of the slider to isFirstResponder of the text field? That would also work

Bindings in VC:

       layout.valueTextField.reactive.text <~ viewModel.valueText.map( { text in
        if !self.layout.valueTextField.isFirstResponder {
            return text
        }
        return self.layout.valueTextField.text ?? ""
        }
    )
    viewModel.value <~ layout.valueSlider.reactive.values
    layout.valueSlider.reactive.value <~ viewModel.value
    viewModel.value <~ layout.valueTextField.reactive.continuousTextValues.map({ textValue in
        if let t = textValue {
            if let parsed = Float(t) {
                return parsed
            }
        }
        return viewModel.value.value
    })

My view Model:

import ReactiveSwift

class ValuePickerViewModel {

var valueText: MutableProperty<String>
var value: MutableProperty<Float>
let minValue: Float
let maxValue: Float
let unit: String

init(initialValue: Float, formatter: FloatFormatter, minValue: Float, maxValue: Float, unit: String) {
    self.value = MutableProperty(initialValue)
    self.valueText = MutableProperty(formatter.format(value: initialValue))
    self.valueText <~ self.value.map({value in
        formatter.format(value: value)
        }
    )
    self.minValue = minValue
    self.maxValue = maxValue
    self.unit = unit
}

}

Solution

  • The solution is simple: The textFields value binds to the view models float value. The slides value binds to the view models text value. So each component updates the other, and there is no cycle!

    Working code:

    Bindings:

        viewModel.value <~ layout.valueTextField.reactive.continuousTextValues.map({ textValue in
            if let t = textValue {
                if let parsed = Float(t) {
                    return parsed
                }
            }
        return viewModel.value.value
        })
        viewModel.valueText <~ layout.valueSlider.reactive.values.map({ value in
            return viewModel.formatter.format(value: value)
        })
        layout.valueSlider.reactive.value <~ viewModel.value
        layout.valueTextField.reactive.text <~ viewModel.valueText
    

    ViewModel:

    import ReactiveSwift
    
    class ValuePickerViewModel {
    
    var valueText: MutableProperty<String>
    var value: MutableProperty<Float>
    let minValue: Float
    let maxValue: Float
    let unit: String
    let formatter: FloatFormatter
    
    init(initialValue: Float, formatter: FloatFormatter, minValue: Float, maxValue: Float, unit: String) {
        self.formatter = formatter
        self.value = MutableProperty(initialValue)
        self.valueText = MutableProperty(formatter.format(value: initialValue))
        self.minValue = minValue
        self.maxValue = maxValue
        self.unit = unit
    }
    
    }