Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Two way binding in RxSwift

I read the two way binding operator in sample code of RxSwift.

func <-> <T>(property: ControlProperty<T>, variable: Variable<T>) -> Disposable {
    let bindToUIDisposable = variable.asObservable()
        .bindTo(property)
    let bindToVariable = property
        .subscribe(onNext: { n in
            variable.value = n
        }, onCompleted:  {
            bindToUIDisposable.dispose()
        })

    return StableCompositeDisposable.create(bindToUIDisposable, bindToVariable)
}

When property changed, it will notify variable, and set the variable's value, while the variable's value is set, it will notify the property. I think it will lead to endless loop...

like image 560
leizh00701 Avatar asked May 28 '16 06:05

leizh00701


People also ask

What is onNext in RxSwift?

- parameter onNext: Action to invoke for each element in the observable sequence. - parameter afterNext: Action to invoke for each element after the observable has passed an onNext event along to its downstream. - parameter onError: Action to invoke upon errored termination of the observable sequence.

What is RxSwift and RxCocoa?

RxSwift is a framework for interacting with the Swift programming language, while RxCocoa is a framework that makes Cocoa APIs used in iOS and OS X easier to use with reactive techniques. ReactiveX frameworks provide a common vocabulary for tasks used repeatedly across different programming languages.

When should I use RxSwift?

RxSwift helps when you need to combine complex asynchronous chains. RxSwift also has types such as Subject, a kind of bridge between the imperative and declarative worlds. The subject can act as an Observable, and at the same time, it can be an Observer, i.e. accept objects and issue events.

What is variable RxSwift?

Variable is a concept added into RxSwift in its early days which basically let you create an imperative bridge by “setting” and “getting” a current value to and from it. It was a seemingly helpful measure to get developers started with RxSwift until they fully understand “Reactive Thinking”.


4 Answers

I believe you can just use bindTo 🙂. Here are implementations for ControlProperty <-> Variable and Variable <-> Variable:

infix operator <-> { precedence 130 associativity left }

func <-><T: Comparable>(property: ControlProperty<T>, variable: Variable<T>) -> Disposable {
    let variableToProperty = variable.asObservable()
        .distinctUntilChanged()
        .bindTo(property)

    let propertyToVariable = property
        .distinctUntilChanged()
        .bindTo(variable)

    return StableCompositeDisposable.create(variableToProperty, propertyToVariable)
}

func <-><T: Comparable>(left: Variable<T>, right: Variable<T>) -> Disposable {
    let leftToRight = left.asObservable()
        .distinctUntilChanged()
        .bindTo(right)

    let rightToLeft = right.asObservable()
        .distinctUntilChanged()
        .bindTo(left)

    return StableCompositeDisposable.create(leftToRight, rightToLeft)
}

Examples of ControlProperty <-> Variable (such as UITextField and UITextView) are in the RxSwiftPlayer project

// Example of Variable <-> Variable

let disposeBag = DisposeBag()
let var1 = Variable(1)
let var2 = Variable(2)

(var1 <-> var2).addDisposableTo(disposeBag)

var1.value = 10
print(var2.value) // 10

var2.value = 20
print(var1.value) // 20
like image 52
Scott Gardner Avatar answered Oct 19 '22 19:10

Scott Gardner


Thanks for raising the question, I spent some time digging around the ControlProperty implementation (note I've added a .debug() call to trace the values generated for control property).

public struct ControlProperty<PropertyType> : ControlPropertyType {
    public typealias E = PropertyType

    let _values: Observable<PropertyType>
    let _valueSink: AnyObserver<PropertyType>

    public init<V: ObservableType, S: ObserverType where E == V.E, E == S.E>(values: V, valueSink: S) {
        _values = values.debug("Control property values").subscribeOn(ConcurrentMainScheduler.instance)
        _valueSink = valueSink.asObserver()
    }

    public func on(event: Event<E>) {
        switch event {
        case .Error(let error):
            bindingErrorToInterface(error)
        case .Next:
            _valueSink.on(event)
        case .Completed:
            _valueSink.on(event)
        }
    }
}

My test setup was as following, I've removed all views positioning here to make it shorter:

import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
    let variable = Variable<Bool>(false);
    let bag = DisposeBag();

    override func loadView() {
        super.loadView()

        let aSwitch = UISwitch();
        view.addSubview(aSwitch)

        (aSwitch.rx_value <-> variable).addDisposableTo(bag);

        let button = UIButton();
        button.rx_tap.subscribeNext { [weak self] in
            self?.variable.value = true;
        }.addDisposableTo(bag)
        view.addSubview(button);
    }
 }

infix operator <-> {
}

func <-> <T>(property: ControlProperty<T>, variable: Variable<T>) -> Disposable{
    let bindToUIDisposable = variable.asObservable().debug("Variable values in bind")
    .bindTo(property)

    let bindToVariable = property
        .debug("Property values in bind")
        .subscribe(onNext: { n in
            variable.value = n
            }, onCompleted:  {
                 bindToUIDisposable.dispose()
        })

    return StableCompositeDisposable.create(bindToUIDisposable, bindToVariable)
 }

Now to the results. First we try tapping the button, which should set the variable to true. This triggers on(event: Event<E>) on ControlProperty and sets the switch value to true.

2016-05-28 12:24:33.229: Variable values in bind -> Event Next(true)

// value flow
value assigned to Variable -> 
Variable emits event -> 
ControlProperty receives event -> 
value assigned to underlying control property (e.g. `on` for `UISwitch`)

Next lets trigger the switch itself. So as we can see, the control generated an event as a result of UIControlEventValueChanged which was passed through _values on ControlProperty, and then its value got assigned to Variable value as in example above. But there's no loop, since update to the Variable value doesn't trigger a control event on the switch.

2016-05-28 12:29:01.957: Control property values -> Event Next(false)
2016-05-28 12:29:01.957: Property values in bind -> Event Next(false)
2016-05-28 12:29:01.958: Variable values in bind -> Event Next(false)

// value flow
trigger the state of control (e.g. `UISwitch`) -> 
ControlProperty emits event -> 
value assigned to Variable -> 
Variable emits event -> 
ControlProperty receives event -> 
value assigned to underlying control property (e.g. `on` for `UISwitch`)

So a simple explanation would be:

  • a value from a control is emitted once some kind of UIControlEvent is triggered
  • when a value is assigned directly to the control property, the control doesn't trigger a change event so there's no loop.

Hope it helps, sorry for a bit messy explanation - I've found it out by experiment)

like image 29
NikGreen Avatar answered Oct 19 '22 18:10

NikGreen


You type anything it will be clear after 5 seconds. This was taken from above answer

import UIKit
import RxSwift
import RxCocoa

class UserViewModel {
    let username = BehaviorSubject<String?>(value: "")
}

class ViewController: UIViewController {

    @IBOutlet weak var email: UITextField!

    var userViewModel = UserViewModel()
    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        userViewModel.username.asObservable().subscribe { print($0) }.disposed(by: bag)
        (email.rx.text <-> userViewModel.username).disposed(by: bag)

        // clear value of the username.
        DispatchQueue.main.asyncAfter(deadline: .now()+5) {
            self.userViewModel.username.onNext(nil)
        }
    }
}

infix operator <->

@discardableResult func <-><T>(property: ControlProperty<T>, variable: BehaviorSubject<T>) -> Disposable {
    let variableToProperty = variable.asObservable()
        .bind(to: property)

    let propertyToVariable = property
        .subscribe(
            onNext: { variable.onNext($0) },
            onCompleted: { variableToProperty.dispose() }
    )

    return Disposables.create(variableToProperty, propertyToVariable)
}
like image 10
Anirudha Mahale Avatar answered Oct 19 '22 18:10

Anirudha Mahale


There is no obstacle to bind BehaviourRelay back to control property. You just need to filter events with the same value (to prevent infinite loop).

For example, in my case, I need to bind email to text field. But I want to remove whitespaces during email input. Here is an example how I achieved it:

emailTextField.rx.text
  .map { $0?.trimmingCharacters(in: CharacterSet.whitespaces) } // remove whitespaces from input
  .bind(to: viewModel.email)
  .disposed(by: disposeBag)

// Just filter all events with no actual value change to prevent infinite loop
viewModel.email
  .filter { $0 != self.emailTextField.text } // if it removed whitespaces in mapping, values will not match and text in text field will be updated
  .bind(to: emailTextField.rx.text)
  .disposed(by: disposeBag)
like image 2
Vitalii Gozhenko Avatar answered Oct 19 '22 18:10

Vitalii Gozhenko