RxSwift in custom views, the clean way
RxSwift let’s you update your views dynamically in your app in many ways – many of which can get messy over time, as I’ve learned the hard way.
The approach that I got used to in order to update views dynamically (and without resorting to crash-prone key-value-observation) was to pass Observable
s to the views. The views then subscribed to them and cleaning up o their own using a DisposeBag
. It worked, but closely coupled the views to RxSwift.
RxSwift has a much cleaner way for doing this – a Binder
. You might be familiar with them through helpers such as myLabel.rx.text
to which you can bind your Observable
s. After using that for a while, I wondered how to expose similar properties for custom views and found the answer.
It’s very simple:
- Implement functions to update your view with regular input data as you would do in a non-reactive world.
- Create a computed property on the view (ideally using a reactive extension) which exposes a
Binder
. - In your view controller, bind your model to that binder (and clean it up there).
Say, you have a tachometer model with a Driver<Measurement<UnitSpeed>>
(regular Observable
works similar) which has a speed value, that you want to bind to a tachometer view.
Your view might have an update function such as:
class TachometerView: UIView {
func update(with speed: Measurement<UnitSpeed>) {
speedLabel.text = speedFormatter.string(from: speed)
}
}
Now create a binder like this:
import RxSwift
import RxCocoa
extension Reactive where Base: TachometerView {
var speed: Binder<Measurement<UnitSpeed>> {
return Binder(self.base) { view, speed in
view.update(with: speed)
}
}
}
And then you can use it like this:
class MyViewController: UIViewController {
override func viewDidLoad() {
// Create + configure view model, call super, ...
viewModel.speed
.drive(tachometerView.rx.speed)
.disposed(by: disposeBag)
}
}
This helps clean-up views and view controllers, and leads to more declarative code, which is great for maintainability.