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 Action
and BindingTarget
and what tasks can be solved with their help. I note right away that for RxSwift these 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
}
}
}
, 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()
}
}
}
: