Введение в RxSwift. Часть 1. Базовые принципы


Логотип проекта ReactiveX

RxSwift — это библиотека для работы с асинхронным кодом, которая представляет события в виде потоков событий с возможностью подписаться на них, а также позволяет применять к ним подходы функционального программирования, что сильно упрощает работу со сложными последовательностями асинхронных событий.

Предпосылкой появления подобных инструментов является то, что в процессе эволюции iOS SDK представил нам широкий выбор инструментов для работы с асинхронными событиями: GCD, KVO, Notification Center, шаблон делегирования и механизм замыканий. Проблема заключается в том, что каждый из этих инструментов требует индивидуального подхода и определенной кодовой базы вокруг них, при этом их сочетание может оказаться не слишком элегантным. API, описываемый проектом ReactiveX — это попытка сделать универсальный язык для работы с асинхронными событиями. Библиотеки, реализующие этот подход, существуют для большого количества языков, так что нельзя сказать, что этот мир ограничивается только мобильной разработкой.

Базовые понятия

Глобально компоненты кода на Rx можно разделить на 3 основных вида: observables, operators и schedulers (обозреваемые последовательности, операторы и планировщики, но далее я буду пользоваться англоязычными вариантами написания).

Observables

Класс Observable<T> является сердцем RxSwift, его назначение состоит в том, чтобы позволить одним классам подписаться на последовательности событий, содержащие данные типа T, которые транслируются другими классами.

Если заглянуть в исходники протокола ObservableType, то там можно обнаружить единственный метод subscribe, который подписывает Observer на приём событий из этой последовательности. Итак, у нас есть некий Observable, транслирующий события, есть какой-то Observer, их слушающий, а что за события такие?

События представляют собой простой enum (см. Event), и бывают трёх видов:

  • next — служит для передачи последнего пришедшего («следующего») значения Observer’у;
  • completed — говорит об успешном завершении последовательности. Это значит, что Observable успешно завершил свой жизненный цикл и больше не будет транслировать события;
  • error — говорит о том, что выполнение Observable завершилось с ошибкой и других событий транслироваться не будет.

Ещё по своей природе Observables могут быть конечными (например, скачивание файла) и бесконечными (например, отслеживание изменения ориентации устройства).

Operators

Реализация класса Observable, содержит некоторое количество вспомогательных методов, которые могут производить различные операции над элементами последовательности. Так как они не имеют побочных эффектов, то их можно комбинировать вместе, что позволяет строить в декларативном стиле достаточно сложную логику. Есть сайт [RxMarbles](https://rxmarbles. com/), который визуализирует работу этих операторов, рекомендую ознакомиться.

Schedulers

Schedulers представляют собой в некотором роде аналог DispatchQueues в мире Rx. Они абстрагируют выполнение задач в различных потоках и очередях, позволяя добиться оптимальной производительности. Это не самая распространенная задача, поэтому не стоит заострять внимание на этой функциональности на первом этапе.

RxCocoa

RxSwift реализует базовое API, описанное в спецификации проекта ReactiveX. Поддержка Rx компонентами iOS SDK, реализована в модуле RxCocoa. Так мы из коробки имеем возможность подписаться на такие действия, как нажатия кнопки, переключение свитчей и т. д.

Практика

Мы разобрались с основными концептами библиотеки, поняли её преимущества, но думаю всё ещё не очень понятно, как воспользоваться этими благами, поэтому разберем практический пример.

Установка RxSwift

Установка мало чем отличается от других библиотек. Я буду пользоваться для этой цели [CocoaPods](http://ovchinnikov. cc/tags/cocoapods/). Добaвим в Podfile следующие строки и запустим pod install:

pod 'RxSwift',    '~> 4.0'
pod 'RxCocoa',    '~> 4.0'

Обрабатываем нажатие кнопки в Rx

Простейший пример с обработкой нажатия кнопки в Rx

Предлагаю рассмотреть самый простой и понятный пример: кинем на контроллер кнопку и лейбл и по нажатию будем увеличивать счётчик. Не буду описывать подробно все действия, сосредоточусь только на том, что касается нашей темы. Для начала импортируем зависимости:

import RxSwift
import RxCocoa

Теперь во viewDidLoad пишем:

tapButton.rx
    .tap
    .subscribe(onNext: { [weak self] in
        self?.count += 1
        self?.countLabel.text = "\(self?.count ?? 0)"
    })
    .disposed(by: disposeBag)

Разберем каждую строчку кода. Как я уже упоминал выше, RxCocoa добавляет нам семейство методов rx, которые содержат уже готовые методы, которые вернут нам ControlEvent, это тип, который удовлетворяет ObservableType, но имеет несколько специфичных особенностей (не может завершиться с ошибкой, выполняется на главном потоке и т.п.). В данном случае нас интересует метод tap (который будет соотвествовать touchUpInside).

Далее мы подписываемся на приём событий, где в параметр onNext передаём замыкание, которое будет выполняться при приходе каждого события. В данном случае мы увеличиваем счётчик и обновляем лейбл. Так как ControlEvent представляет собой по сути бесконечную последовательность, то параметров onError и onComplete у него не будет. На самом деле, мы можем средствами RxSwift привязать и значения лейбла к значениям переменной, но об этом поговорим в будущих статьях.

Отдельно стоит упомянуть про disposeBag (aka сумка утилизации). Когда мы закончили работать с последовательностью, что у должно появится закономерное желание освободить память. Когда, мы смотрели исходники ObservableType, можно было обратить внимание, что метод subscribe возвращает объект типа Disposable. Если посмотреть его протокол, окажется, что он требует единственный метод dispose, который, как можно догадаться из названия, и служит этой цели. Но диспозить объекты вручную может быть утомительно, поэтому для этого и был придуман класс DisposeBag.

Открыв его исходник, мы видим, что в методе deinit он вызывает метод dispose, в котором далее пробегается в цикле по всем объектам, добавленным в сумку утилизации и вызывает dispose у них. Соотвествено, когда disposeBag «потеряет» ссылку на наш ViewController, т. е. её retainCount станет равным нулю, у неё будет вызван метод dealloc, который очистит все объекты, что в итоге избавляет нас от необходимости думать об утечках памяти при подписке на события.

Бонусом, чтобы до конца осознать преимущества «магии» Rx, предлагаю представить следующую гипотетическую ситуацию: к тебе приходит менеджер и говорит, что по некоторым причинам, нужно запретить пользователям нажимать на кнопку чаще чем раз в 1 секунду. Без Rx нужно будет придумывать какие-то костыли с таймером, а в нашем случае понадобится лишь воспользоваться соответствующим оператором и увеличить код всего на одну строчку:

tapButton.rx
    .tap
    .throttle(1.0, scheduler: MainScheduler.instance)
    .subscribe(onNext: { [weak self] in
        self?.count += 1
        self?.countLabel.text = "\(self?.count ?? 0)"
    })
    .disposed(by: disposeBag)

Если в данном случае ситуация высосана из пальца, то в случае с вводом поисковую строку, когда надо дать пользователю время на ввод нескольких символов, прежде чем отправлять запрос на сервер, использование оператора throttle может оказаться весьма полезным.

Сегодня мы очень коротко пробежались по основным понятиям мира Rx и RxSwift, в следующих статьях я постараюсь разобрать каждое из них подробнее, а также рассмотреть архитектуры приложений, которые могут получиться с использованием этой библиотеки.

Исходники примера