Falco Winkler
Falco Winkler

Reputation: 1190

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:

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
}

}

Upvotes: 0

Views: 529

Answers (1)

Falco Winkler
Falco Winkler

Reputation: 1190

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
}

}

Upvotes: 1

Related Questions