[Разработка под iOS, Swift] Action и BindingTarget в ReactiveSwift
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет, Хабр!Меня зовут Игорь, я руковожу отделом мобайла в AGIMA. Еще не все перешли с ReactiveSwift/Rxswift на Combine? Тогда сегодня я расскажу про опыт использования таких концептов из ReactiveSwift как Action и BindingTarget и какие задачи можно решить с их помощью. Сразу отмечу, что для RxSwift эти же концепции существует в виде RxAction и Binder. В статье рассмотрим, примеры на ReactiveSwift и в конце я покажу, как все то же самое выглядит на RxSwift.Рассчитываю на то, что вы уже представляете, что такое реактивное программирование и имели опыт с ReactiveSwift или RxSwift.Представим, что у нас есть страница продукта и кнопка добавления в избранное. Когда мы нажимаем ее, вместо нее начинает крутиться лоадер, и по результатам кнопка становится либо залитой, либо нет. Скорее всего, у нас будет что-то подобное во ViewController (используем 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()
}
}
И во 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()
}
Лучше? На мой взгляд, да. Теперь давайте разбираться, что такое ActionAction представляет собой фабрику для 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 в свою очередь, позволяет инкапсулировать код, отвечающий за биндинг и вместе эти концепции делают код более элегантным, читаемым и надежным, чего все мы пытаемся достичь :)И обещанный перевод на RxSwiftViewController:
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()
}
}
}
Ссылочки:ActionRxSwiftCommunity/Action
===========
Источник:
habr.com
===========
Похожие новости:
- Выпуск Porteus Kiosk 5.1.0, дистрибутива для оснащения интернет-киосков
- [Разработка под iOS, Разработка мобильных приложений, Swift, Аналитика мобильных приложений] Автоматизация тестирования продуктовой аналитики в мобильных приложениях
- [Компьютерное железо, Процессоры] Производители материнских плат выложили обновления прошивки BIOS под процессоры Ryzen 5000-й серии
- [Программирование] Интеграция библиотеки на Swift в UE4
- [Информационная безопасность, Разработка под iOS, Системы обмена сообщениями] Apple потребовала от Telegram заблокировать три белорусских канала
- [IT-компании, Законодательство в IT, Монетизация игр, Разработка под iOS] Судебное разбирательство по иску Epic Games против Apple начнется в мае 2021 года
- [Тестирование мобильных приложений, Разработка под iOS] Cucumber и BDD. Пишем UI-автотесты на iOS
- [Разработка под iOS, Разработка под Android, Карьера в IT-индустрии, Конференции, Flutter] 22 октября приглашаем на онлайн-митап Hot Mobile: iOS, Android, Flutter
- [Разработка под iOS, Управление персоналом, Карьера в IT-индустрии] Сценарий идеального технического собеседования
- [Разработка под iOS, Разработка мобильных приложений, Разработка под Android, Монетизация мобильных приложений] На Apps Live 2020 вас ждет не только классика — будем завоёвывать Поднебесную
Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_swift, #_xcode, #_ios, #_mvvm, #_reactive, #_reactivex, #_reactivecocoa, #_swift, #_blog_kompanii_agentstvo_agima (
Блог компании Агентство AGIMA
), #_razrabotka_pod_ios (
Разработка под iOS
), #_swift
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 21:26
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет, Хабр!Меня зовут Игорь, я руковожу отделом мобайла в AGIMA. Еще не все перешли с ReactiveSwift/Rxswift на Combine? Тогда сегодня я расскажу про опыт использования таких концептов из ReactiveSwift как Action и BindingTarget и какие задачи можно решить с их помощью. Сразу отмечу, что для RxSwift эти же концепции существует в виде RxAction и Binder. В статье рассмотрим, примеры на ReactiveSwift и в конце я покажу, как все то же самое выглядит на RxSwift.Рассчитываю на то, что вы уже представляете, что такое реактивное программирование и имели опыт с ReactiveSwift или RxSwift.Представим, что у нас есть страница продукта и кнопка добавления в избранное. Когда мы нажимаем ее, вместо нее начинает крутиться лоадер, и по результатам кнопка становится либо залитой, либо нет. Скорее всего, у нас будет что-то подобное во ViewController (используем 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() } } 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 } } } 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() } Основной (но не весь!) функционал представлен на листинге ниже. 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>) } let toggleFavorite: BindingTarget<Void> = favoriteAction.bindingTarget
viewModel.toggleFavorite <~ button.reactive.controlEvents(.touchUpInside)
isLoadingSignal
.take(duringLifetimeOf: self) .observe { [weak self] isLoading in isLoading ? self?.showLoadingView() : self?.hideLoadingView() } self.reactive.isLoading <~ isLoadingSignal
extension Reactive where Base: ViewController {
var isLoading: BindingTarget<Bool> { makeBindingTarget { (vc, isLoading) in isLoading ? vc.showLoadingView() : vc.hideLoadingView() } } } var isLoading = false
... reactive[\.isLoading] <~ isLoadingSignal 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) 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 } extension Reactive where Base: UIViewController {
var isLoading: Binder<Bool> { Binder(self.base) { vc, value in value ? vc.showLoadingView() : vc.hideLoadingView() } } } =========== Источник: habr.com =========== Похожие новости:
Блог компании Агентство AGIMA ), #_razrabotka_pod_ios ( Разработка под iOS ), #_swift |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 21:26
Часовой пояс: UTC + 5