Action and BindingTarget in ReactiveSwift
I’m Igor, Head of the Mobile Department at AGIMA. Well, haven’t everyone switched from ReactiveSwift/RxSwift to Combine yet? Then today I will talk about our experience of using such ReactiveSwift concepts as Action and BindingTarget, as well as about what tasks can be solved with their help. Please keep in mind that the same concepts exist as RxAction and Binder in RxSwift. In this article, we will look at examples for ReactiveSwift, and at the end I will show the same things for RxSwift.
I hope that you are already familiar with reactive programming and have experience using ReactiveSwift or RxSwift.
Let’s say we have a product page and a button to add a product to favorites. When we press the button, a loader starts spinning instead of it, and as a result, the button becomes either filled with a certain color or not. Most likely, we will have something like this in the ViewController (we are using the MVVM architecture).
And in the viewModel:
All looks good, but the number of MutableProperty and “manual” state management is a little confusing and provides additional possibilities for errors. That’s where Action comes in. Thanks to it, we can make our code more reactive and get rid of “unnecessary” manual state management. There are 2 ways to launch an Action: launch apply method or use BindingTarget (we’ll talk about this a little later). Let’s discuss the first option. Now our code for the viewModel will look like this:
Better? In my opinion, it is. Now let’s discuss what Action is.
Action is a factory for SignalProducer that allows you to monitor all its events (for RxSwift adepts: SignalProducer is a cold signal, and Signal is a hot signal). Action takes a value as input and passes it to the execute block, which returns a SignalProducer.
The main (but not all!) functionality is in the listing below.
Why do we need all this? values is a stream of all values from the Action, and errors contains all errors. isExecuting tells us if an action is currently in progress (perfect for loaders). The most important thing here is that values and errors have Never error type, that is, they will never terminate, which allows us to safely use them in reactive chains. isEnabled: Action has on/off states, which prevents concurrent execution. This can be useful when we need to prevent 10 button presses in a row. Actually, on/off states for the Action provide quite flexible control, but to be honest, I actually never had to use it, so we won’t discuss it here :)
Now we don’t need to process the SignalProducer results, since we get them in the favoriteAction.values signal. If we need to handle errors, we can use the favoriteAction.errors signal for that.
Important Note 1: The apply method returns a new SignalProducer each time; however, values, errors, and isExecuting are independent of this and receive events from all producers created inside their Action.
Important Note 2: Action is executed serially. We cannot execute the Action several times in a row without waiting for the previous action to complete. Otherwise, we will get an error saying that the Action is not available (this is also true for RxSwift).
Now let’s look at the second way to trigger an Action using the BindingTarget. In the viewModel, we no longer need the toggleFavorite method, and it is transformed as follows:
let toggleFavorite: BindingTarget<Void> = favoriteAction.bindingTarget
The code in the view controller will be like this:
viewModel.toggleFavorite <~ button.reactive.controlEvents(.touchUpInside)
It looks quite familiar. This is our favorite binding operator. The left side is the BindingTarget.
However, there is one caveat: sometimes we would like to cancel the execution of the SignalProducer, for example, when we download a file and click сancel button. Usually, after launching SignalProducer or subscribing to Signal, we would save Disposable and call its dispose() method. If we pass input values through the binding operator, then the SignalProducer is launched inside the Action and we do not have access to Disposable.
What is BindingTarget? BindingTarget is a structure that contains a 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 as BindingTarget.
It looks pretty elegant. In general, the BindingTarget is a very useful thing to “train” objects to process data threads inside themselves and save you from having to use the following code over and over again:
Instead, you just need to write:
self.reactive.isLoading <~ isLoadingSignal
The good news: the framework takes care of completing the subscription, and we don’t have to worry about that.
The isLoading declaration will look as follows (all existing bindings look exactly the same):
Note that in the make BindingTarget method, you can specify in which thread the binding will be called. There is another option that implies the use of KeyPath (only in the main thread):
var isLoading = false...reactive[\.isLoading] <~ isLoadingSignal
The above ways of using BindingTarget are available only for classes and are part of ReactiveCocoa. In general, there are other capabilities, but, in my opinion, in 99% of cases this will be enough.
Action acts as a great helper for building reactive chains and feels great inside the ViewModel layer. BindingTarget, in turn, allows you to encapsulate the binding code, and together these concepts make the code more elegant, readable, and reliable. And this is exactly the goal that we all try to achieve :)
RxSwift translation as promised: