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
We’ll look at the following two use cases:
binding 2 textfields and subscribing to each other’s
text
control 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
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)
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 betweentextFieldFirst
andtextFieldSecond
and 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)
}
It binds theBehaviourRelay
property andControlPropery
to 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 thatControlProperty
have built-in mechanism that stops event to not be emitted twice
-> BehaviourRelay -> ConrolProperty ----> X -----> BehaviourRelay
How does it work
When send send event onBehaviourRelay
,ControlPropery
updates because of the binding
let bindToUIDisposable = relay.bind(to: property)
When we send event onControlPropery
,BehaviourRelay
updates because of the subscription
let bindToRelay = property
.subscribe(onNext: { n in
relay.accept(n)
}, onCompleted: {
bindToUIDisposable.dispose()
})
// 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`)
So a simple why there is no loop:
a value from a control is emitted once some kind of
UIControlEvent
is triggeredwhen 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 theBehaviourRelay
property.
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)
-> textFirst BehaviourRelay -> textSecond BehaviourRelay -> textFirst BehaviourRelay -> textSecond BehaviourRelay -> textFirst BehaviourRelay -> textSecond BehaviourRelay .....
Apparently ifBehaviourRelay
does 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)
}
}
}
}
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 (filter
RxSwift 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)
}
})
}
and then use it like that
let textFirst = BehaviorRelay(value: nil)
let textSecond = BehaviorRelay(value: nil)
(textSecond <-> textFirst).disposed(by: disposeBag)
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 intextFirst
andtextSecond
the 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)
Link to the Example repository
Top comments (0)