Bonjour, Habr!
Je m'appelle Igor, je suis responsable du département mobile chez AGIMA. Tout le monde n'est pas encore passé de ReactiveSwift / Rxswift à Combine? Puis aujourd'hui, je parlerai de l'expérience de l'utilisation de concepts tels que le ReactiveSwift Action
et des BindingTarget
tâches qui peuvent être résolues avec leur aide. Je note tout de suite que pour RxSwift les mêmes concepts existent dans la forme RxAction
et Binder
. Dans l'article, nous examinerons des exemples sur ReactiveSwift et à la fin je montrerai comment tout se ressemble sur RxSwift.
J'espère que vous savez déjà ce qu'est la programmation réactive et que vous avez de l'expérience avec ReactiveSwift ou RxSwift.
Disons que nous avons une page produit et un bouton Ajouter aux favoris. Lorsque nous appuyons dessus, le chargeur commence à tourner à sa place et, par conséquent, le bouton est rempli ou non. Très probablement, nous aurons quelque chose comme ça dans le ViewController (en utilisant l'architecture MVVM).
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()
}
}
Et dans la vueModèle:
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()
}
}
}
: