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 SignalProducerwith 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? valuesis a stream of all values ​​from Action errors– all errors. isExecutingshows 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 applyreturns a new one every time SignalProducer but values , errors, isExecutingthey do not depend on this and receive events from all producers created within their Action

Important point 2: Actionis 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 SignalProducersince 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 Disposableand 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, Observerand MutablePropertycan 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 isLoadingwill 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 makeBindingTargetyou 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.

Actionacts 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:

Action

RxSwiftCommunity / Action

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *