RxSwift Reverse observable aka two way binding

vaderdan

Danny L

Posted on December 27, 2018

RxSwift Reverse observable aka two way binding

When we hear Reactive programming we usually think about listeners of Observable sequences, transformers and combining data, and reacting on changes.

So.. RxSwift is about passing data from the business logic to views, right? but how about passing events in both directions

TextField <------> Observable <------> TextField
Enter fullscreen mode Exit fullscreen mode

We’ll look at the following two use cases:

  • binding 2 textfields and subscribing to each other’stextcontrol property (when change the text in one of them, the other automatically updates)

  • go next level and make first/last/full name form that updates text like on the picture above

Let’s get started!

Existing libraries and approaches

Before starting out and coding I sometimes like to check if I did’n reinvent the hot water — do we have some existing libraries or something else done related to the topic.

RxBiBinding

And… I found this library

RxSwiftCommunity/RxBiBinding

that does excellent job. I just have to to connect two textfields like so

import RxBiBinding

let disposeBag = DisposeBag()

var textFieldFirst = UITextField()
var textFieldSecond = UITextField()

(textFieldFirst.rx.text <-> textFieldSecond.rx.text).disposed(by: disposeBag)
Enter fullscreen mode Exit fullscreen mode

and it will listen for changes in the textfields, and update textfields texts in both directions.

And it is good enough for the most simple use case - sending text betweentextFieldFirstandtextFieldSecondand back as it is.This library does not provide a way to map and modify the passed sequence ofStrings.

And in the real world we don’t pass observable sequences as it is, we often map/transform it (for example: if I would like to pass only numbers and filter out letters…)

RxSwift main repository examples folder.

The next approach I found was in the examples folder of RxSwift

https://github.com/ReactiveX/RxSwift/blob/master/RxExample/RxExample/Operators.swift#L17

// Two way binding operator between control property and relay, that's all it takes.
infix operator <-> : DefaultPrecedence

func <-> (property: ControlProperty, relay: BehaviorRelay) -> Disposable {
    let bindToUIDisposable = relay.bind(to: property)
    let bindToRelay = property
        .subscribe(onNext: { n in
            relay.accept(n)
        }, onCompleted:  {
            bindToUIDisposable.dispose()
        })

    return Disposables.create(bindToUIDisposable, bindToRelay)
}
Enter fullscreen mode Exit fullscreen mode

It binds theBehaviourRelayproperty andControlProperyto each other, and it sends updates in both directions to properties as expected.

I was worried that it will cause loop (because of binding properties to each other)(relay send events to control property, and control property send the same to subscribed relay, then relay send to control property same event ….forever), but it appears thatControlPropertyhave built-in mechanism that stops event to not be emitted twice

-> BehaviourRelay -> ConrolProperty ----> X -----> BehaviourRelay
Enter fullscreen mode Exit fullscreen mode

How does it work

When send send event onBehaviourRelay ,ControlProperyupdates because of the binding

let bindToUIDisposable = relay.bind(to: property)
Enter fullscreen mode Exit fullscreen mode

When we send event onControlPropery ,BehaviourRelayupdates because of the subscription

let bindToRelay = property
        .subscribe(onNext: { n in
            relay.accept(n)
        }, onCompleted:  {
            bindToUIDisposable.dispose()
        })
Enter fullscreen mode Exit fullscreen mode
// value flowtrigger the state of control (e.g. `UITextField`) 
-> ControlProperty emits event 
-> value assigned to BehaviourRelay 
-> BehaviourRelay emits event 
-> ControlProperty receives event 
-> value assigned to underlying control property (e.g. `text` for `UITextField`)
Enter fullscreen mode Exit fullscreen mode

So a simple why there is no loop:

  • a value from a control is emitted once some kind ofUIControlEventis triggered

  • when a value is assigned directly to the control property, the control doesn’t trigger a change event so there’s no loop.

This approach satisfies our needs and we could modify text before subscribing theBehaviourRelayproperty.

Our Example

However the previous example won’t work well if we bind to each other twoBehaviourRelay — it will cause event loop

let textFirst = BehaviorRelay(value: nil)
let textSecond = BehaviorRelay(value: nil)

(textSecond <-> textFirst).disposed(by: disposeBag)
Enter fullscreen mode Exit fullscreen mode
-> textFirst BehaviourRelay -> textSecond BehaviourRelay -> textFirst BehaviourRelay -> textSecond BehaviourRelay -> textFirst BehaviourRelay -> textSecond BehaviourRelay .....
Enter fullscreen mode Exit fullscreen mode

Apparently ifBehaviourRelaydoes not have built-in way to stop passing same event to it’s subscribers over and over so we are going to build that mechanism.

Previous Observable value

I did this little convenience helper to get previous sequence values of Observable

extension ObservableType {
  func currentAndPrevious() -> Observable<(current: E, previous: E)> {
    return self.multicast({ () -> PublishSubject in PublishSubject() }) { (values: Observable) -> Observable<(current: E, previous: E)> in
      let pastValues = Observable.merge(values.take(1), values)

      return Observable.combineLatest(values.asObservable(), pastValues) { (current, previous) in
        return (current: current, previous: previous)
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

I need this because I need to tell whichObservable(TextField text) was changed

The example

I have two textfields and I’ll have to get the value from changed field (with old value != current) and update textfield that is unchanged (with old value == current)

And If I don’t want to cause forever loop I’ll have to check that the current values of the fields are equal, to stop propagating evens (filterRxSwift operator)

infix operator <->

func <-> (lhs: BehaviorRelay, rhs: BehaviorRelay) -> Disposable {
    typealias ItemType = (current: T, previous: T)

    return Observable.combineLatest(lhs.currentAndPrevious(), rhs.currentAndPrevious())
        .filter({ (first: ItemType, second: ItemType) -> Bool in
            return first.current != second.current
        })
        .subscribe(onNext: { (first: ItemType, second: ItemType) in
            if first.current != first.previous {
                rhs.accept(first.current)
            }
            else if (second.current != second.previous) {
                lhs.accept(second.current)
            }
        })
}
Enter fullscreen mode Exit fullscreen mode

and then use it like that

let textFirst = BehaviorRelay(value: nil)
let textSecond = BehaviorRelay(value: nil)
(textSecond <-> textFirst).disposed(by: disposeBag)
Enter fullscreen mode Exit fullscreen mode

More complex Example

This is the full code for two way binding between first and last and full name text fields (like on the animated gif on top)

When we enter text intextFirstandtextSecondthe lastname field (textFull) is updated with concatenated first and last name texts.


let textFull = BehaviorRelay(value: nil)
let textFirst = BehaviorRelay(value: nil)
let textSecond = BehaviorRelay(value: nil)


typealias ItemType = (current: String, previous: String)

Observable.combineLatest(textFirst.map({ $0 ?? "" }).currentAndPrevious(), textSecond.map({ $0 ?? "" }).currentAndPrevious(), textFull.map({ $0 ?? "" }).currentAndPrevious())
      .filter({ (first: ItemType, second: ItemType, full: ItemType) -> Bool in
        return "\(first.current) \(second.current)" != full.current && "\(first.current)" != full.current
      })
      .subscribe(onNext: { (first: ItemType, second: ItemType, full: ItemType) in
        if first.current != first.previous || second.current != second.previous {
          textFull.accept("\(first.current) \(second.current)")
        }
        else if (full.current != full.previous) {
          let items = full.current.components(separatedBy: " ")
          let firstName = items.count > 0 ? items[0] : ""
          let lastName = items.count > 1 ? items[1] : ""

          if firstName != first.current {
            textFirst.accept(firstName)
          } else if lastName != second.current {
            textSecond.accept(lastName)
          }
        }
      })
      .disposed(by: disposeBag)


(textFieldFirst.rx.text <-> textFirst).disposed(by: disposeBag)
(textFieldSecond.rx.text <-> textSecond).disposed(by: disposeBag)
(textFieldFull.rx.text <-> textFull).disposed(by: disposeBag)
Enter fullscreen mode Exit fullscreen mode

Link to the Example repository

https://github.com/vaderdan/Example2WayBinding

💖 💪 🙅 🚩
vaderdan
Danny L

Posted on December 27, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related