Upgrade to Pro — share decks privately, control downloads, hide ads and more …

CodeFest 2019. Дмитрий Тримонов (Яндекс) — Rx в современной мобильной разработке. Есть ли смысл?

CodeFest
April 06, 2019

CodeFest 2019. Дмитрий Тримонов (Яндекс) — Rx в современной мобильной разработке. Есть ли смысл?

В докладе речь пойдет о целесообразности использования ФРП (Functional Reactive Programming) в современной мобильной разработке на примере RxSwift под iOS. Еще год назад мне казалось, что невозможно писать код без использования Rx, а все самописные велосипеды, призванные в очередной раз реализовать паттерн Observer — от лукавого. Во всех проектах, в которых мне довелось поучаствовать, в числе которых две версии Яндекс.Карт под Windows Phone и iOS, Rx стал одной из главных библиотек, насквозь пронизывающей все слои приложения. В последнее время моё отношение к Rx стало гораздо более прагматичным в силу ряда причин, о которых я расскажу в докладе.

Цель моего доклада — поглубже посмотреть на устройство одного из самых популярных фреймворков для ФРП под iOS — RxSwift и на его примере показать, что Rx как и любой инструмент — это лишь удобный способ решения определенного спектра задач. Мы поговорим о best practices при использовании Rx, тонкостях использования различных компонентов любого современного Rx фреймворка, таких как Subject, Scheduler, Trait и других, рассмотрим способы тестирования сложных time-dependent фич, коснемся вопросов о контрактах Rx и многом другом, а главное, попытаемся понять, помогут ли все эти знания сделать наш проект более понятным, поддерживаемым и bug-free.

CodeFest

April 06, 2019
Tweet

More Decks by CodeFest

Other Decks in Technology

Transcript

  1. 1

  2. Введение в Reactive Extension › Push vs Pull, FRP, история

    создания Rx - отличный инструмент › пишем тестируемый реактивный код › делаем тесты быстрыми Проблемы при использовании › гайдлайны vs реальность › устройство фреймворка План 3
  3. Pull - запрашиваем следующую порцию данных как только они нам

    понадобились › Iterator Push - получаем оповещение о новой порции данных от поставщика как только данные готовы › Delegate › Observer Push vs Pull подходы 5
  4. Функциональное › иммутабельные структуры данных › «чистые функции» - функции

    без side-эффектов Реактивное › данные - потоки событий с аргументами во времени (aka streams) › синхронный и асинхронный код воспринимается одинаково FRP как структура приложения 6
  5. Откуда появился Observable 8 один элемент несколько элементов синхронно T

    getItem() Enumerable<T> getItems() асинхронно Task<T> getItem() Observable<T> getItems()
  6. 〉NotificationCenter 〉Delegate patter 〉Grand Central Dispatch 〉Closures Зачем может понадобиться

    Rx 11 Унификация при работе с различными асинхронными API
  7. › Microsoft Reactive Extensions (Rx .NET) - 2009 › Сообщество

    (reactivex.io) определило общее API для имплементаций на разных языках программирования › RxSwift 2015 История создания 12
  8. ReactiveSwift, ReactiveCocoa › решает некоторые проблемы Reactive Extensions API ReSwift

    › накладывает жесткие ограничения на способ написания приложения PromiseKit › другая абстракция Альтернативы RxSwift 13
  9. Простота внедрения › возможность поэтапного внедрения › порог вхождения Ускорение

    процесса разработки › упрощение реализации фичей › повышение читаемости, тестируемости, … кода Сложность отказа/замены › зависимость проекта от «предоставляемых услуг» Как оценить инструмент? 15
  10. Простота внедрения › возможность поэтапного внедрения › порог вхождения Ускорение

    процесса разработки › упрощение реализации фичей › повышение читаемости, тестируемости, … кода Сложность отказа/замены › зависимость проекта от «предоставляемых услуг» Как оценить инструмент? 16
  11. Условия автоматического скрывания › Пользователь превысил скорость 2.5 м/с при

    включенном режиме автовращения › Пользователь включил режим автовращения при скорости, превышающей 2.5 м/с Авто-скрывание шторки 22
  12. Возвращает Observable событий, когда нужно закрыть шторку по простой логике

    Реализация func hideEvents(didChangeAutoRotationMode: Observable<Bool>, didUpdateSpeed: Observable<Double>) -> Observable<Void> { let speedIsAboveThreshold = didUpdateSpeed.map { $0 > 2.5 } .distinctUntilChanged() let autoRotationIsOn = didChangeAutoRotationMode .distinctUntilChanged() return Observable.combineLatest(speedIsAboveThreshold, autoRotationIsOn) .filter { $0.0 && $0.1 } .mapTo(()) }
  13. 24

  14. createHotObservable › создает стрим по списку событий в абсолютные моменты

    времени createColdObservable › создает стрим по списку событий в относительные моменты времени start › создает TestObserver › создает Observable, подписывается и уничтожает подписку в моменты времени: created, subscribed, disposed TestScheduler 26
  15. TestScheduler.start 27 t
 tcreate
 Создание Observable tsubscribe
 Подписка tdispose
 Отписка

    Создание TestObserver scheduleAbsoluteVirtual(_:time) для каждого t
  16. Тесты простой логики func testWhenSpeedExceedsThresholdAndAutoRotationIsOnThenDrawerHides() { let autoRotationMode = testScheduler.createHotObservable(

    [.next(300, false), .next(600, true)] ) let speed = testScheduler.createHotObservable( [.next(400, 1.5), .next(800, 2.51)] ) let hideEventsObserver = testScheduler.start { () -> Observable<Void> in return SimpleDrawerHidingStrategy().hideEvents( didChangeAutoRotationMode: autoRotationMode.asObservable(), didUpdateSpeed: speed.asObservable() ) } XCTAssert(hideEventsObserver.events.count == 1) XCTAssert(hideEventsObserver.events[0].time == 800) }
  17. Тесты простой логики func testWhenSpeedExceedsThresholdAndAutoRotationIsOnThenDrawerHides() { let autoRotationMode = testScheduler.createHotObservable(

    [.next(300, false), .next(600, true)] ) let speed = testScheduler.createHotObservable( [.next(400, 1.5), .next(800, 2.51)] ) let hideEventsObserver = testScheduler.start { () -> Observable<Void> in return SimpleDrawerHidingStrategy().hideEvents( didChangeAutoRotationMode: autoRotationMode.asObservable(), didUpdateSpeed: speed.asObservable() ) } XCTAssert(hideEventsObserver.events.count == 1) XCTAssert(hideEventsObserver.events[0].time == 800) }
  18. Тесты простой логики func testWhenSpeedExceedsThresholdAndAutoRotationIsOnThenDrawerHides() { let autoRotationMode = testScheduler.createHotObservable(

    [.next(300, false), .next(600, true)] ) let speed = testScheduler.createHotObservable( [.next(400, 1.5), .next(800, 2.51)] ) let hideEventsObserver = testScheduler.start { () -> Observable<Void> in return SimpleDrawerHidingStrategy().hideEvents( didChangeAutoRotationMode: autoRotationMode.asObservable(), didUpdateSpeed: speed.asObservable() ) } XCTAssert(hideEventsObserver.events.count == 1) XCTAssert(hideEventsObserver.events[0].time == 800) }
  19. Усложним первое условия автоматического скрывания › Пользователь превысил скорость 2.5

    м/с при включенном режиме автовращения, после чего прошло 5 секунд, в течение которых скорость не падала ниже 2.5 м/с и режим автовращения не был выключен Авто-скрывание шторки 31
  20. 32

  21. Реализация сложной логики func hideEvents(didChangeAutoRotationMode: Observable<Bool>, didUpdateSpeed: Observable<Double>) -> Observable<Void>

    { let autoRotationIsOn = didChangeAutoRotationMode .distinctUntilChanged() let autoRotationDidTurnOn = autoRotationIsOn.filter { $0 } let autoRotationDidTurnOff = autoRotationIsOn.filter { !$0 } let speedIsAboveThreshold = didUpdateSpeed .map { $0 > 2.5 } .distinctUntilChanged() let speedUp = speedIsAboveThreshold.filter { $0 } let speedDown = speedIsAboveThreshold.filter { !$0 } ...
  22. Реализация сложной логики func hideEvents(didChangeAutoRotationMode: Observable<Bool>, didUpdateSpeed: Observable<Double>) -> Observable<Void>

    { let autoRotationIsOn = didChangeAutoRotationMode .distinctUntilChanged() let autoRotationDidTurnOn = autoRotationIsOn.filter { $0 } let autoRotationDidTurnOff = autoRotationIsOn.filter { !$0 } let speedIsAboveThreshold = didUpdateSpeed .map { $0 > 2.5 } .distinctUntilChanged() let speedUp = speedIsAboveThreshold.filter { $0 } let speedDown = speedIsAboveThreshold.filter { !$0 } ...
  23. Реализация сложной логики ... let speedUpWhileAutoRotationIsOn = speedUp .withLatestFrom(autoRotationIsOn) .filter

    { $0 } let timerFor5Sec = Observable<Int>.timer(5.0, scheduler: ...) let timeShouldStop = Observable<Void> .merge(autoRotationDidTurnOff, speedDown) let speedCondition = speedUpWhileAutoRotationIsOn .flatMapLatest { _ -> Observable<Void> in return timerFor5Sec.takeUntil(timeShouldStop) } ... return Observable.merge(autoRotationCondition, speedCondition) }
  24. Реализация сложной логики ... let speedUpWhileAutoRotationIsOn = speedUp .withLatestFrom(autoRotationIsOn) .filter

    { $0 } let timerFor5Sec = Observable<Int>.timer(5.0, scheduler: ...) let timeShouldStop = Observable<Void> .merge(autoRotationDidTurnOff, speedDown) let speedCondition = speedUpWhileAutoRotationIsOn .flatMapLatest { _ -> Observable<Void> in return timerFor5Sec.takeUntil(timeShouldStop) } ... return Observable.merge(autoRotationCondition, speedCondition) }
  25. Реализация сложной логики ... let speedUpWhileAutoRotationIsOn = speedUp .withLatestFrom(autoRotationIsOn) .filter

    { $0 } let timerFor5Sec = Observable<Int>.timer(5.0, scheduler: ...) let timeShouldStop = Observable<Void> .merge(autoRotationDidTurnOff, speedDown) let speedCondition = speedUpWhileAutoRotationIsOn .flatMapLatest { _ -> Observable<Void> in return timerFor5Sec.takeUntil(timeShouldStop) } ... return Observable.merge(autoRotationCondition, speedCondition) }
  26. Проверяем, что шторка закроется именно через 5 секунд Тесты сложной

    логики let autoRotationMode = testScheduler.createHotObservable( [.next(300, false), .next(600, true)] ) let speed = testScheduler.createHotObservable( [.next(400, 1.5), .next(800, 2.51)] ) let hideEventsObserver = testScheduler.start { () -> Observable<Void> in return SmartDrawerHidingStrategy().hideEvents(...) } XCTAssert(hideEventsObserver.events[0].time == 805)
  27. Тесты сложной логики Проверяем, что шторка не закроется при падении

    скорости let autoRotationMode = testScheduler.createHotObservable( [.next(300, false), .next(600, true)] ) let speed = testScheduler.createHotObservable( [.next(400, 1.5), .next(800, 2.51), .next(804, 2.49)] ) let hideEventsObserver = testScheduler.start { () -> Observable<Void> in return SmartDrawerHidingStrategy().hideEvents(...) } XCTAssert(hideEventsObserver.events.isEmpty)
  28. Тесты сложной логики Проверяем, что шторка не закроется при выключении

    автовращения let autoRotationMode = testScheduler.createHotObservable( [.next(300, false), .next(600, true), .next(804, false)] ) let speed = testScheduler.createHotObservable( [.next(400, 1.5), .next(800, 2.51)] ) let hideEventsObserver = testScheduler.start { () -> Observable<Void> in return SmartDrawerHidingStrategy().hideEvents(…) } XCTAssert(hideEventsObserver.events.isEmpty)
  29. Должны реагировать на движения карты › Поиск должен начинаться только

    после завершении анимации камеры (drag, pinch, …) › Поиск должен отменяться при старте анимации в момент поиска Поиск в картах 42
  30. 43

  31. Абстрагируем способ получения событий об изменении позиции камеры и способ

    поиска Протоколы protocol Map: AnyObject { var mapCameraEvents: Observable<MapCameraEventArgs> { get } } protocol MapKudaGoSearchAPI { func searchEvents(with ...) -> Observable<Result<[KudaGoEvent]> }
  32. Возвращает Observable событий об изменении состояния экрана MapKudaGoSearchViewModel class MapKudaGoSearchViewModel

    { var didChangeScreenState: Observable<ScreenState> { return screenState.asObservable() } init(map: Map, searchApi: MapKudaGoSearchAPI) { ... } ... private let screenState = PublishRelay<ScreenState>() }
  33. Реализация логики поиска let stops = map.mapCameraEvents.filter { $0.state ==

    .finished } let starts = map.mapCameraEvents.filter { $0.state == .started } let searchRequests = stops.debounce(0.5) .withLatestFrom(map.mapCameraEvents) .filter { $0.state != .started } searchRequests .do(onNext: { _ in screenState.accept(.loading) }) .flatMapLatest { [weak self] args in let cancels = starts.do(onNext: { screenState.accept(.canceled) }) return self?.searchApi.searchEvents(args).takeUntil(cancels) ?? ... } .map { result -> ScreenState in ... } .bind(to: screenState)
  34. Тесты логики отмены поиска Проверяем, что поиск отменяется при старте

    анимации камеры let mapCameraEvents = testScheduler.createHotObservable([ .next(300, MapCameraEventArgs(mapCamera: ..., state: .finished)), .next(400, MapCameraEventArgs(mapCamera: ..., state: .started)) ]) let searchResult = testScheduler.createHotObservable([ .next(500, Result<[KudaGoEvent], APIError>.success([])) ]) let sut = MapKudaGoSearchViewModel(...) let stateObserver = testScheduler.start { () -> Observable<ScreenState> in return sut.didChangeScreenState } XCTAssert(stateObserver.events[1] == .next(400, ScreenState.canceled))
  35. Тесты логики отмены поиска Проверяем, что поиск отменяется при старте

    анимации камеры let mapCameraEvents = testScheduler.createHotObservable([ .next(300, MapCameraEventArgs(mapCamera: ..., state: .finished)), .next(400, MapCameraEventArgs(mapCamera: ..., state: .started)) ]) let searchResult = testScheduler.createHotObservable([ .next(500, Result<[KudaGoEvent], APIError>.success([])) ]) let sut = MapKudaGoSearchViewModel(...) let stateObserver = testScheduler.start { () -> Observable<ScreenState> in return sut.didChangeScreenState } XCTAssert(stateObserver.events[1] == .next(400, ScreenState.canceled))
  36. Тесты логики отмены поиска Проверяем, что поиск отменяется при старте

    анимации камеры let mapCameraEvents = testScheduler.createHotObservable([ .next(300, MapCameraEventArgs(mapCamera: ..., state: .finished)), .next(400, MapCameraEventArgs(mapCamera: ..., state: .started)) ]) let searchResult = testScheduler.createHotObservable([ .next(500, Result<[KudaGoEvent], APIError>.success([])) ]) let sut = MapKudaGoSearchViewModel(...) let stateObserver = testScheduler.start { () -> Observable<ScreenState> in return sut.didChangeScreenState } XCTAssert(stateObserver.events[1] == .next(400, ScreenState.canceled))
  37. Тесты логики отмены поиска Проверяем, что поиск отменяется при старте

    анимации камеры let mapCameraEvents = testScheduler.createHotObservable([ .next(300, MapCameraEventArgs(mapCamera: ..., state: .finished)), .next(400, MapCameraEventArgs(mapCamera: ..., state: .started)) ]) let searchResult = testScheduler.createHotObservable([ .next(500, Result<[KudaGoEvent], APIError>.success([])) ]) let sut = MapKudaGoSearchViewModel(...) let stateObserver = testScheduler.start { () -> Observable<ScreenState> in return sut.didChangeScreenState } XCTAssert(stateObserver.events[1] == .next(400, ScreenState.canceled))
  38. Выделяйте удобные абстракции › входные потоки данных › способ выполнения

    - Scheduler Выполняйте side-эффекты явно › в подписках › используя оператор do 51 Рекомендации
  39. Плюсы › декларативность › однообразность › легко тестировать Минусы ›

    для понимания необходимо сразу знать все операторы › требуется «въехать» в парадигму › сложно отказаться Итоги 52
  40. Используем виртуальное время 56 DispatchWorkItem VirtualSchedulerItem async(after:) scheduleRelative(dueTime:) workItem.cancel() workItem.dispose()

    Unit of work Schedule Cancel DispatchQueue TestScheduler TestScheduler - очередь для выполнения задач в определенные моменты времени, но с виртуальным временем
  41. Рекомендации 57 › Покрывайте асинхронное поведение тестами, использующими виртуальное время

    › Подключайте RxTest в таргет тестов и используйте TestScheduler для получения «быстрых» тестов
  42. Несоответствием концепций и гайдлайнов с реальностью › кэширует ли стрим

    значение? › как получить текущее значение стрима? › как понять, горячий это стрим или холодный? › как показать, что мой Observable не эмитит ошибок? Проблемы вызванные 59
  43. Цель контрактов › гарантия определенного поведения у интерфейсов › предусловия,

    постусловия, инварианты Способы создания контрактов › на уровне абстракций (протоколов) - система типов › на уровне реализаций (классов) - тесты Контрактное программирование 61
  44. Это, по сути, формальное определение Observable › подписчику может быть

    отправлено ноль или более .next событий › отправив .error или .completed событие, последовательность завершается и не может больше производить других элементов Гайдлайны. Контракт Observable 62 Валидные стримы: Невалидные стримы:
  45. В реальности нужно больше гарантий › Observable - слишком общая

    абстракция › по интерфейсу нельзя понять «горячий» стрим или «холодный» › вынуждены описывать реализацию в комментариях к протоколам или использовать специализированные стримы (Traits*) › для разных реализаций стрима нужна разная обработка (цепочка операторов) Проблема контракта в Rx 63 * - Driver, Single, Completable, Signal
  46. Определяется на основе конвенции и системы типов Контракт API protocol

    CountriesRepository { func fetchCountries(@escaping completion: ([Country]) -> Void) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) repo.fetchCountries { [weak self] in self?.update(withNewCountries: $0) }) } единственное значение без ошибки
  47. Пробуем сделать реактивным Идеальный мир protocol CountriesRepository { func fetchCountries()

    -> Observable<[Country]> } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) repo.fetchCountries().subscribe(onNext: { [weak self] in self?.update(withNewCountries: $0) }) } возможна ошибка не гарантировано одно значение
  48. Описываем контракт и надеемся, что он будет удовлетворен Реальность. Комментарии

    protocol CountriesRepository { // Нет ошибок // Единственное значение func fetchCountries() -> Observable<[Country]> }
  49. Используем Trait для «усиления» контракта, но от комментариев не уйти

    Реальность. Trait + комментарии protocol CountriesRepository { // Нет ошибок func fetchCountries() -> Single<[Country]> }
  50. Новое требование - отображать кол-во стран в плейсхолдере Идеальный мир

    override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let countriesStream = repo.fetchCountries() countriesStream.subscribe(onNext: { ... }) countriesStream.map { "\($0.count) countries" } .subscribe(onNext: { placeholder in self.searchBar.placeholder = placeholder }) } подписка 1 подписка 2
  51. Вынуждены завязываться на реализацию стрима и явно “шарить” Завязка на

    реализацию override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let countriesStream = repo.fetchCountries().publish() countriesStream.subscribe(onNext: { ... }) countriesStream.map { "\($0) countries" } .subscribe(onNext: { ... }) countriesStream.connect() }
  52. Контрактов и гайдлайнов не всегда достаточно Проблема 71 Приходится выбирать

    - лишний код или комментарии Снижение читаемости кода или повышение вероятности ошибки Повышается сложность и стоимость рефакторинга
  53. › Определяйте контракты на уровне протоколов с помощью Traits, а

    на уровне реализаций - тестов › Не используйте Rx там, где его контрактов недостаточно или нужны более жесткие гарантии Рекомендации 72
  54. Непониманием устройства используемого фреймворка › особенности реализации примитивов RxSwift ›

    Scheduler или как применять observeOn, subscribeOn › переход в парадигму Rx и обратно › создание оператора, которого нет в Rx › когда нужно «шарить» стримы › где использовать Traits вместо простого Observable › и множество других… Проблемы вызванные 73
  55. Непониманием устройства используемого фреймворка › особенности реализации примитивов RxSwift ›

    Scheduler или как применять observeOn, subscribeOn › переход в парадигму Rx и обратно › создание оператора, которого нет в Rx › когда нужно «шарить» стримы › где использовать Traits вместо простого Observable › и множество других… Проблемы вызванные 74
  56. Подписчики › нужно ли ретейнить самостоятельно Операторы и Subjects ›

    когда создаются › кто отвечает за освобождение ресурсов Подписки › нужно ли сохранять › вызывать dispose самому или использовать DisposeBag Особенности реализации примитивов RxSwift 76
  57. › Ресурсы операторов освобождаются при получении терминальных событиях или уничтожении

    подписок › При совпадении времени жизни объекта и подписки удобно использовать DisposeBag Выводы 79
  58. BehaviorSubject › подписчикам не форвардятся события, если Subject получал .error

    или .completed до момента подписки › подписчик ретейнится до вызова dispose у самого сабджекта или подписки, полученной при вызове subscribe PublishSubject › аналогично за исключением того, что ссылки на подписчиков удаляются при получении терминального события Особенности Subject 81
  59. Особенность BehaviorSubject let subject = BehaviorSubject<Bool>(value: false) subject.onCompleted() _ =

    subject.subscribe(PrintObserver<Bool>()) subject.onNext(true) subject.onNext(false) PrintObserver<Bool> did receive: completed PrintObserver<Bool> deallocated Переходит в состояние Disposed. Перестает «прокидывать»
  60. Подписка ControlEvent на PublishSubject Пример из жизни class WayPointViewModel {

    ... var taps = PublishSubject<Void>() } class WayPointCellView: GenericBindableView<WayPointViewModel> { override func bind(to viewModel: WayPointViewModel) { ... clearButton.rx.tap.bind(to: viewModel.taps).disposed(by: binding) } }
  61. Используем statefull вью- модели в MVVM Проблема 86 Время жизни

    вью оказывалось меньше чем у вью-модели PublishSubject получал .completed от ControlEvent при deinit вью
  62. › Читайте документацию для типов, которые используете › Обновляйте RxSwift.

    Переходите на Relay вместо Subject › Используйте Observable.create вместо Relay, Subject где возможно Рекомендации 87
  63. По-умолчанию код RxSwift однопоточный › используется CurrentThreadScheduler › код подписок

    и операторов выполняется на потоке, на котором выполнен subscribe Управление выполняется за счет › использования observeOn, subscribeOn › передачи scheduler некоторым операторам Scheduler. Принципы 90
  64. subscribeOn › указываем scheduler, на котором будет выполнена подписка Observer’а,

    идущего ниже по цепочке на Observable идущий выше по цепочке observerOn › указывает скедулер, на котором будет вызван on(event:) у Observer’а, идущего ниже по цепочке Управление 91
  65. Поведение по-умолчанию func just(n: Int) -> Observable<Int> { return Observable<Int>.create

    { observer -> Disposable in log("subscribed") observer.onNext(3)) observer.onCompleted() return Disposables.create() } } just(3).subscribe(onNext: { (num) in log("onNext: \(num)”) }) [12:51:42]com.apple.main-thread subscribed [12:51:45]com.apple.main-thread onNext: 3
  66. subscribeOn just(n: 10) .subscribeOn(DispatchQueueScheduler(.background)) .subscribe(onNext: { (num) in log("onNext: \(num)”)

    }) [8:01:03]thread: <NSThread: 0x600000274580> subscribed [8:01:03]thread: <NSThread: 0x600000274580> onNext: 10 «Прорастает» вверх по цепочке операторов
  67. Порядок исполнения при subscribeOn 95 Вызов SubscribeOnObservable.subscribe Создание JustObservable Создание

    SubscribeOnObservable SubscribeOnSink.onNext Вызов onNext JustObservable.subscribe(SubscribeOnSink) скедулинг background queue main-thread
  68. observeOn just(n: 10) .subscribeOn(DispatchQueueScheduler(.background)) .observeOn(MainScheduler.instance) .subscribe(onNext: { (num) in log("onNext:

    \(num)”) }) [8:01:03]thread: <NSThread: 0x600000274580> subscribed [8:01:03]com.apple.main-thread onNext: 10 «Действует» вниз по цепочке операторов до следующего observeOn
  69. Рекомендации 97 › Не «зашивайте» логику скедулинга на верхних уровнях

    приложения (сервисы) › Старайтесь держать subscribeOn и observeOn рядом и как можно ближе к Presentation слою › Применяйте скедулинг когда это действительно нужно или для тестов
  70. Перевод в Rx парадигму 98 Асинхронные методы с completion-блоками ›

    Observable.fromAsync «Listeners API» › Observable.create + Listener, который форвардит подписчику нотификации Расширения для системного и собственного API › extension Reactive<Base> where Base: ...
  71. Можно взять отсюда или подключить RxSwiftExt Асинхронные методы с completion-блоками

    extension Observable { static func fromAsync(_ asyncRequest: @escaping (@escaping (Element) -> Void) -> Void) -> Observable<Element> { return Observable.create({ (o) -> Disposable in asyncRequest({ (result) in o.onNext(result) o.onCompleted() }) return Disposables.create() }) } }
  72. Готовые «переходы» в Rx парадигму 100 UIButton › tap UIControl

    › isEnabled, isSelected NotificationCenter › notification(name:) и многие другие…
  73. Расширяем интерфейс существующих компонентов, делая его Реактивное API с помощью

    RxSwift extension Reactive where Base: TimeInfoView { public var date: Binder<Date> { return Binder(self.base) { element, value in element.date = value } } } Observable<Int> .interval(1.0) .map { _ in Date() } .bind(to: timeInfoView.rx.date) .disposed(by: bag)
  74. Расширяем интерфейс существующих компонентов, делая его Реактивное API с помощью

    RxSwift extension Reactive where Base: TimeInfoView { public var date: Binder<Date> { return Binder(self.base) { element, value in element.date = value } } } Observable<Int> .interval(1.0) .map { _ in Date() } .bind(to: timeInfoView.rx.date) .disposed(by: bag)
  75. › Переиспользуйте компоненты, а не инфраструктуру/архитектуру › Перед написанием extensions

    к типам из Cocoa проверьте, возможно они там уже есть › Повторяйте поведение, реализованное в библиотеке Рекомендации 103
  76. Есть ли смысл? Да Отличный инструмент для решения ограниченного ряда

    задач › работа с потоками данных и манипуляциями над ними › отложенное выполнение задач › логика сильно завязана на время Обладает рядом преимуществ › декларативный, тестируемый, поддерживаемый код › хорошая абстракция для concurrency 104
  77. Есть ли смысл? Нет Может усложнить работу › скрывает синхронное

    выполнение за абстракцией потока › невозможно на уровне API представить поток без ошибок › код сложнее отлаживать › начинает «прорастать» в код, который можно написать проще Привносит ряд ограничений › увеличивается размер приложения 105
  78. 106