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'll talk about the experience of the use of such concepts as the ReactiveSwift Actionand BindingTargetand what tasks can be solved with their help. I note right away that for RxSwift these concepts exist in the form RxActionand 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
    }
  }
}

, MutableProperty ยซยป , . Action . ยซยป . Action 2- : SignalProducer apply BindingTarget( ). , viewModel :

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()
}

? , . , Action

Action SignalProducer ( RxSwift: SignalProducer โ€” , Signal โ€” ). Action , execute , SignalProducer.

( !) .

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>)
}

? values Action errorsโ€” . isExecuting , ( ). , values errors Never ยซยป, .  isEnabled- Action / , . , 10 . , ยซยป Action , , , , :)

1:  apply SignalProducer   values , errors, isExecuting , Action

2: Action . Action , . , , Action ( RxSwift).

SignalProducer, favoriteAction.values , favoriteAction.errors

2- Action BindingTarget viewModel toggleFavorite :

let toggleFavorite: BindingTarget<Void> = favoriteAction.bindingTarget

viewModel.toggleFavorite <~ button.reactive.controlEvents(.touchUpInside)

. . BindingTarget.

E, , : SignalProducer, , - . , SignalProducer Signal Disposable dispose(). input , SignalProducer Action disposable .

BindingTarget? BindingTarget ,

, Lifetime(, ). , Observer MutableProperty BindingTarget.

. , BindingTargetโ€” , ยซยป :

isLoadingSignal
    .take(duringLifetimeOf: self)
    .observe { [weak self] isLoading in 
        isLoading ? self?.showLoadingView() : self?.hideLoadingView()
    }

:

self.reactive.isLoading <~ isLoadingSignal

โ€” , .

isLoading ( ):

extension Reactive where Base: ViewController {
    var isLoading: BindingTarget<Bool> {
        makeBindingTarget { (vc, isLoading) in
            isLoading ? vc.showLoadingView() : vc.hideLoadingView()
        }
    }
}

, makeBindingTarget , .  KeyPath ( ):

var isLoading = false

...

reactive[\.isLoading] <~ isLoadingSignal

BindingTarget ReactiveCocoa , , , , 99% .

Action ยซยป ViewModel .  BindingTarget , , , , :)

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()
        }
    }
}

:

Action

RxSwiftCommunity/Action




All Articles