Action and BindingTarget in ReactiveSwift
Hello, Habr!
My name is Igor, I am the head of the mobile department at AGIMA. Not everyone switched from ReactiveSwift / Rxswift to Combine yet? Then today I will tell you about the experience of using such concepts from ReactiveSwift as Action
and BindingTarget
and what tasks can be solved with their help. I note right away that for RxSwift the same concepts exist in the form RxAction
and Binder
… In the article, we will consider examples on ReactiveSwift and at the end I will show how everything looks the same on RxSwift.
I hope that you already know what reactive programming is and have experience with ReactiveSwift or RxSwift.
Let’s say we have a product page and an add to favorites button. When we press it, the loader starts spinning instead of it, and as a result, the button becomes either filled or not. Most likely, we will have something like this in the ViewController (using the MVVM architecture).
let favoriteButton = UIButton()
let favoriteLoader = UIActivityIndicatorView()
let viewModel: ProductViewModel
func viewDidLoad() {
...
favoriteButton.reactive.image <~ viewModel.isFavorite.map(mapToImage)
favoriteLoader.reactive.isAnimating <~ viewModel.isLoading
// Скрыть кнопку во время выполнения запрос
favoriteButton.reactive.isHidden <~ viewModel.isLoading
favoriteButton.reactive.controlEvents(.touchUpInside)
.take(duringLifetimeOf: self)
.observeValues { [viewModel] _ in
viewModel.toggleFavorite()
}
}
And in the viewModel:
lazy var isFavorite = Property(_isFavorite)
private let _isFavorite: MutableProperty<Bool>
lazy var isLoading = Property(_isLoading)
private let _isLoading: MutableProperty<Bool>
func toggleFavorite() {
_isLoading.value = true
service.toggleFavorite(product).startWithResult { [weak self] result in
self._isLoading.value = false
switch result {
case .success(let isFav):
self?.isFavorite.value = isFav
case .failure(let error):
// do somtething with error
}
}
}
Everything would be fine, but the amount is a little confusing MutableProperty
and the amount of “manual” state management, which creates additional room for error. This is where it helps us Action
… Thanks to him, we can make our code more reactive and get rid of “unnecessary” code. Run Action
can be done in 2 ways: run SignalProducer
from method apply
directly and using BindingTarget
(more on that later). Let’s consider the first option, now the code for the viewModel will look like this:
let isFavorite: Property<Bool>
let isLoading: Property<Bool>
private let toggleAction: Action<Void, Bool, Error>
init(product: Product, service: FavoritesService = FavoriteServiceImpl()) {
toggleAction = Action<Void, Bool, Error> {
service.toggleFavorite(productId: product.id)
.map { $0.isFavorite }
}
isFavorite = Property(initial: product.isFavorite, then: toggleAction.values)
isLoading = toggleAction.isExecuting
}
func toggleFavorite() {
favoriteAction.apply().start()
}
Better? In my opinion, yes. Now let’s figure out what is Action
Action
is a factory for SignalProducer
with the ability to observe all its events (for RxSwift adepts: SignalProducer is a cold signal, Signal is a hot one). Action
takes a value as input, passes it to the execute block, which returns SignalProducer.
The main (but not all!) Functionality is presented in the listing below.
final class Action<Input, Output, Error> {
let values: Signal<Output, Never>
let errors: Signal<Error, Never>
let isExecuting: Property<Bool>
let isEnabled: Property<Bool>
var bindingTarget: BindingTarget<Input>
func apply(_ input: Input) -> SignalProducer<Output, Error> {...}
init(execute: @escaping (T, Input) -> SignalProducer<Output, Error>)
}
Why do you need all this? values
is a stream of all values from Action
errors
– all errors. isExecuting
shows us if an action is currently in progress (ideal for loaders). The most valuable thing here is that values
and errors
have an error type Never
that is, they never crash, which allows us to safely use them in reactive chains. isEnabled
– Action has on / off states, which gives us protection against concurrent execution. It can be useful when we need to protect ourselves from 10 button presses in a row. In general, managing the “inclusion” of Action is quite flexible, but to tell the truth, I never had to use it, so this will not be included in the article 🙂
Important point 1: method apply
returns a new one every time SignalProducer
but values
, errors
, isExecuting
they do not depend on this and receive events from all producers created within their Action
Important point 2: Action
is executed sequentially. We cannot run Action
several times in a row, without waiting for the previous action to be completed. In this case, we will receive an error saying that Action
unavailable (also true for RxSwift).
Now it is not necessary to process the results SignalProducer
since we receive them in the signal favoriteAction.values
If you need to handle errors, you can use the signal favoriteAction.errors
Now let’s look at the 2nd way to trigger an Action with BindingTarget
In viewModel, we no longer need a method toggleFavorite
it transforms in this way into this:
let toggleFavorite: BindingTarget<Void> = favoriteAction.bindingTarget
The code in the view controller will become like this
viewModel.toggleFavorite <~ button.reactive.controlEvents(.touchUpInside)
It looks painfully familiar. This is our favorite binding operator. The left side is BindingTarget.
There is, however, one caveat: sometimes we would like to cancel the execution of the SignalProducer, for example, we download a file and click on the cancel button. Usually, launching SignalProducer or subscribing to Signal, we would save Disposable
and called its dispose () method. If we supply input values through the binding operator, then the SignalProducer is launched inside the Action and we do not have access to the disposable.
What is BindingTarget
? BindingTarget
is a structure containing
block that will be called when a new value is received and the so-called Lifetime
(an object that reflects the lifetime of the object). By the way, Observer
and MutableProperty
can also be used like BindingTarget
…
It will turn out quite elegantly. Generally, BindingTarget
– this is a very useful thing in order to “teach” objects to process data streams within themselves and not write again:
isLoadingSignal
.take(duringLifetimeOf: self)
.observe { [weak self] isLoading in
isLoading ? self?.showLoadingView() : self?.hideLoadingView()
}
and instead write:
self.reactive.isLoading <~ isLoadingSignal
The good news is that the framework takes care of completing the subscription, and we don’t have to worry about that.
Announcement isLoading
will look like this (all existing bindings look exactly the same):
extension Reactive where Base: ViewController {
var isLoading: BindingTarget<Bool> {
makeBindingTarget { (vc, isLoading) in
isLoading ? vc.showLoadingView() : vc.hideLoadingView()
}
}
}
Note that in the method makeBindingTarget
you can specify on which thread the binding will be called. There is another option using KeyPath (only on the main thread):
var isLoading = false
...
reactive[.isLoading] <~ isLoadingSignal
The above uses BindingTarget
are only available for classes and are part of ReactiveCocoa
In general, these are not all possibilities, but, in my opinion, in 99% of cases this will be enough.
Action
acts as an excellent helper for building “eternal” reactive chains and feels great on the ViewModel layer. BindingTarget
in turn, allows you to encapsulate the code responsible for the binding and together these concepts make the code more elegant, readable and reliable, which is what we all try to achieve 🙂
And the promised transfer to RxSwift
ViewController:
viewModel.isFavorite
.map(mapToImage)
.drive(favoriteButton.rx.image())
.disposed(by: disposeBag)
viewModel.isLoading
.drive(favoriteLoader.rx.isAnimating)
.disposed(by: disposeBag)
viewModel.isLoading
.drive(favoriteButton.rx.isHidden)
.disposed(by: disposeBag)
favoriteButton.rx.tap
.bind(to: viewModel.toggleFavorite)
.disposed(by: disposeBag)
ViewModel
let isFavorite: Driver<Bool>
let isLoading: Driver<Bool>
let toggleFavorite: AnyObserver<Void>
private let toggleAction = Action<Void, Bool>
init(product: Product, service: FavoritesService = FavoriteServiceImpl()) {
toggleAction = Action<Void, Bool> {
service.toggleFavorite(productId: product.id)
.map { $0.isFavorite }
}
isFavorite = toggleAction.elements.asDriver(onErrorJustReturn: false)
isLoading = toggleAction.executing.asDriver(onErrorJustReturn: false)
toggleFavorite = toggleAction.inputs
}
Binder
extension Reactive where Base: UIViewController {
var isLoading: Binder<Bool> {
Binder(self.base) { vc, value in
value ? vc.showLoadingView() : vc.hideLoadingView()
}
}
}
Links: