Около года назад наш iOS-разработчик компании Noveo, Александр, заинтересовался RxSwift. Столкнувшись с нехваткой документации, Саша решил самостоятельно упорядочить все приобретенные в ходе изучения фреймворка знания — для себя и для других. Результатом стала одна из его статей о Swift 2.2. Со времени ее публикации, конечно, и RxSwift, и сам Swift эволюционировали, и материал нуждался в обновлении, и другой iOS-разработчик из Noveo, Михаил, адаптировал материал для Swift 3. После редакции статья заиграла свежими красками, и на этом мы передаем слово нашим коллегам.
Заинтересовавшись темой функционального программирования, я встал на распутье: какой фреймворк выбрать для ознакомления? ReactiveCocoa — ветеран в iOS-кругах, информации по нему вдоволь. Но он вырос из Objective-C, и хотя это не является проблемой, все же в данный момент я в основном пишу на Swift, и хотелось бы взять решение, изначально спроектированное с учетом всех плюшек этого языка. RxSwift же — порт Reactive Extensions; последний, конечно, имеет долгую историю, но сам порт свежий и написан как раз под Swift. На нем я и решил остановиться.
Как выяснилось, документация по RxSwift несколько специфична: описание всех команд ведет на http://reactivex.io/, а там в основном дается общая информация, у разработчиков еще не дошли руки сделать документацию именно для RxSwift, что не всегда удобно. Некоторые команды имеют тонкости в реализации, а есть такие, о которых в общей документации нет ничего, кроме упоминания.
Прочитав все главы вики с гитхаба RxSwift, я решил сразу поразбираться с официальными примерами; тут-то и стало ясно, что с RX такое не пройдет, нужно хорошо понимать основы, иначе будешь как мартышка с копипастом гранатой. Я начал разбирать самые сложные для понимания команды, а потом перешел к тем, что были вроде и понятны, но задавая себе вопросы по ним, я лишь догадывался, как верно ответить, и уверенности в моих ответах у меня не было.
В общем, я решил проработать все операторы RxSwift. Лучший способ что-то понять в программировании — запустить код и посмотреть, как он отработает. Учитывая специфику реактивного программирования, лучше дополнить это схемами (они бывают очень полезны), ну и кратким описанием на русском. Закончив сегодня работу, я подумал, что грех не поделиться результатами с тем, кто лишь присматривается к теме реактивного программирования.
Много картинок и текста ниже, очень много!
Предварительно я рекомендую просмотреть официальную документацию, у меня передана основная суть и специфика RxSwift команд, а не основы.
Так же можно “поиграться” с шариками в схемах, так называемые RxMarbles, есть бесплатная версия под iPhone/iPad.
Итак, в этой статье я рассмотрю все (ну или почти все) команды RxSwift, дам для каждой краткое описание, схему (если это имеет смысл), код, результат выполнения, а при необходимости сделаю комментарии по выводу в лог результатов выполнения кода.
В статье заголовок каждой команды — ссылка на на официальную документацию, т.к. я не ставил перед собой цели перевести все нюансы по командам.
Вот и ссылка конкретно на PDF, где в виде mindMap собраны все команды, что позволяет быстро просмотреть их все. Кусочки кода в PDF приложены для того, чтобы увидеть, как и с каким параметрами нужно работать с командой. Изначально ради этого PDF я все и затеял — чтобы иметь под рукой документ, в котором наглядно видны все команды с их схемами. PDF получился огромным (в плане рабочего пространства, а не веса), но я проверял, даже на iPad 2 все нормально просматривается.
Обо всех ошибках просьба писать в личку, объем работ оказался слегка великоват, после четвертой вычитки текста мои глаза меня прокляли.
Что ж, надеюсь, моя работа кому-то пригодится. Приступим.
Содержание
Создание Observable
asObservable
create
deferred
empty
error
interval
just
never
of
range
repeatElement
timer
Комбинирование Observable
amb
combineLatest
concat
merge
startWith
switchLatest
withLatestFrom
zip
Фильтрация
distinctUntilChanged
elementAt
filter
ignoreElements
sample
single
skip
skip (duration)
skipUntil
skipWhile
skipWhileWithIndex
take
take (duration)
takeLast
takeUntil
takeWhile
takeWhileWithIndex
debounce
Трансформация
buffer
flatMap
flatMapFirst
flatMapLatest
flatMapWithIndex
map
mapWithIndex
window
Операторы математические и агрегирования
Работа с ошибками
catchError
catchErrorJustReturn
retry
retryWhen
Операторы для работы с Connectable Observable
multicast
publish
refCount
replay
replayAll
Вспомогательные методы
debug
do
delaySubscription
observeOn
subscribe
subscribeOn
timeout
using
В схемах я буду использовать обозначение Source/SO в качестве Source Observable, RO/Result в качестве Result Observable.
Функция example просто позволяет отделять вывод в консоли, её код следующий (взят из RxSwift):
Во всех примерах, где необходимо работать с временными задержками, если этот код будет запускаться в песочнице, необходимо прописать
Также подразумевается, что читатель имеет общее представление о реактивном программировании в общем и об RxSwift в частности. Не знаю, есть ли смысл городить очередную вводную.
Создание Observable
asObservable
Этот метод реализован в классах RxSwift, если они поддерживают конвертацию в Observable
. Например: ControlEvent
, ControlProperty
, Variable
, Driver
Консоль:
В данном примере мы Variable
преобразовали в Observable
и подписались на его события.
create
Этот метод позволяет создавать Observable
с нуля, полностью контролируя, какие элементы и когда он будет генерировать.
Консоль:
В данном примере мы создали Observable
, который сгенерирует несколько значений, и в конце вызовется complete
.
deferred
Этот оператор позволяет отложить создание Observable
до момента подписки с помощью subscribe
.
Консоль:
В первом случае Observable
создается сразу, с помощью Observable.just(i)
, и изменение значения i уже не влияет на генерируемый этой последовательностью элемент. Во втором же случае мы создаем Observable
с помощью deferred
и можем поменять значение i перед subscribe
.
empty
Пустая последовательность, заканчивающаяся Completed.
Консоль:
error
Создаст последовательность, которая состоит из одного события – Error
.
Консоль:
interval
Создает бесконечную последовательность, возрастающую с 0 с шагом 1 с указанной периодичностью.
Консоль:
just
Создает последовательность из любого значения, которая завершается Completed
.
Консоль:
never
Пустая последовательность, чьи observer’ы никогда не вызываются, т.е. не будет сгенерировано ни одно событие.
Консоль:
of
Последовательность из переменного количества элементов. После всех элементов генерируется Completed
.
Консоль:
В первом случае мы создали последовательность из двух чисел, во втором — из двух Observable
, а затем объединили их между собой с помощью оператора merge
.
range
Создает последовательность с конечным числом элементов, возрастающую с шагом 1 от указанного значения указанное число раз, после всех элементов генерируется Completed
.
Консоль:
Сгенерировались элементы, начиная с 5, 3 раза с шагом 1.
repeatElement
Бесконечно создавать указанный элемент, без задержек. Никогда не будут сгенерированы события Completed
или Error
.
Консоль:
timer
Бесконечная последовательность, возрастающая с 0 с шагом 1, с указанной периодичностью и возможностью задержки при старте. Никогда не будут сгенерированы события Completed
или Error
.
Консоль:
В данном примере последовательность начнет генерировать элементы с задержкой в 2 секунды, каждые 3 секунды.
Комбинирование Observable
amb
Из всех Observable
SO выбирается тот, который первым начинает генерировать элементы, его элементы и дублируются в RO, остальные SO игнорируются.
Консоль:
Т.к. первым сгенерировал элемент subjectC, лишь его элементы дублируются в RO, остальные игнорируются.
combineLatest
Как только все Observable
сгенерировали хотя бы по одному элементу, эти элементы используются в качестве параметров в переданную функцию, и результат этой функции генерируется RO в качестве элемента. В дальнейшем при генерации элемента любым Observable
генерируется новый результат функции с последними элементами из всех комбинируемых Observable
.
Консоль:
В этом примере я создал Observable
с помощью timer
— для генерации элементов с разной задержкой, чтобы было видно, как перемешиваются элементы. К моменту появления первого элемента sequenceA
появилось уже три элемента sequenceB
. Поэтому первым элементом в RO последовательности стала пара объектов A0 – B2.
concat
В RO элементы включают сначала все элементы первого Observable
, и лишь затем следующего. Это означает, что если первый Observable
никогда не сгенерирует Completed
, элементы второго никогда не поступят в RO. Ошибка в текущем Observable
пробрасывается в RO.
Консоль:
merge
Элементы RO включают элементы из исходных Observable
в том порядке, в котором они были выпущены в исходных Observable
.
Консоль:
Последовательности сделаны с задержкой в генерации, и видно, что элементы в RO теперь вперемешку, в том порядке, в котором они были сгенерированы в исходных Observable
.
startWith
В начало SO добавляются элементы, переданные в качестве аргумента.
Консоль:
switchLatest
Изначально подписываемся на O1 генерируемого SO, его элементы зеркально генерируются в RO. Как только из SO генерируется очередной Observable
, элементы предыдущего Observable
отбрасываются, т.к. происходит отписка от O1, подписываемся на O2 и так далее. Таким образом в RO — элементы лишь из последнего сгенерированного Observable
.
Консоль:
В первом примере показано, как команда работает в статике, когда мы руками переподключаем Observable
.
Во втором примере оператором create создан Observable<Observable>
, AnyObserver
которого мы вынесли в переменную, по таймеру получающую новый Observable
, который реализован в виде таймера. Т.к. задержки у таймеров разные, то можно наблюдать. как с помощью switchLatest
в RO попадают значения из последнего сгенерированного таймера.
withLatestFrom
Как только O1 генерирует элемент, проверяется, сгенерирован ли хоть один элемент в O2, и если да, то берутся последние элементы из O1 и O2 и используются в качестве аргументов для переданной функции, результат которой генерируется RO в качестве элемента.
Консоль:
zip
Элементы RO представляют собой комбинацию из элементов, сгенерированных исходными Observable
, объединение идет по индексу выпущенного элемента.
Консоль:
Из примеров видно, что элементы комбинируются попарно в том порядке, в каком они были сгенерированы в исходных Observable
.
Фильтрация
distinctUntilChanged
Пропускаем все повторяющиеся подряд идущие элементы.
Консоль:
Здесь тонкий момент: отбрасываются не уникальные для всей последовательности элементы, а лишь те, которые идут подряд.
elementAt
В RO попадает лишь элемент, выпущенный N по счету.
Консоль:
filter
Отбрасываются все элементы, которые не удовлетворяют заданным условиям.
Консоль:
ignoreElements
Отбрасывает все элементы, передаёт только терминальные сообщения Completed
и Error
.
Консоль:
sample
При каждом сгенерированном элементе последовательности семплера (воспринимать как таймер) — брать последний выпущенный элемент исходной последовательности и дублировать его в RO, ЕСЛИ он не был сгенерирован ранее.
Консоль:
single
Из исходной последовательности берется единственный элемент, если элементов > 1 — генерировать ошибку. Есть вариант с предикатом.
Консоль:
В первом примере в исходной последовательности оказалось больше 1 элемента, поэтому была сгенерирована ошибка в момент генерирования в SO второго элемента.
Во втором примере условиям предиката удовлетворил всего 1 элемент, поэтому ошибки сгенерировано не было.
skip
Из SO отбрасываем первые N элементов.
Консоль:
skip (duration)
Из SO отбрасываем первые элементы, которые были сгенерированы в первые N.
Консоль:
skipUntil
Отбрасываем из SO элементы, которые были сгенерированы до начала генерации элементов последовательностью, переданной в качестве параметра.
Консоль:
Генерация элементов в secondSequence
была отложена на 1 секунду с помощью команды delaySubscription
, таким образом элементы из firstSequence
стали дублироваться в RO лишь через 1 секунду.
skipWhile
Отбрасываем из SO элементы до тех пор, пока функция, переданная в качестве параметра, возвращает true.
Консоль:
skipWhileWithIndex
Отбрасываем из SO элементы до тех пор, пока пока функция, переданная в качестве параметра, возвращает true. Отличие от skipWhile
в том, что еще одним параметром, переданным в функцию, является индекс сгенерированного элемента.
Консоль:
take
Из SO берутся лишь первые N элементов.
Консоль:
take (duration)
Из SO берутся лишь элементы, сгенерированные в первые N секунд.
Консоль:
takeLast
Из SO берутся лишь последние N элементов. Что означает: если SO никогда не закончит генерировать элементы, в RO не попадет ни одного элемента.
Консоль:
Второй пример приведен для иллюстрации в задержке генерации элементов в RO из-за ожидания завершения генерации элементов в SO.
takeUntil
Из SO берутся элементы, которые были выпущены до начала генерации элементов последовательностью, переданной в качестве параметра.
Консоль:
takeWhile
Из SO берутся элементы до тех пор, пока функция, переданная в качестве параметра, возвращает true.
Консоль:
takeWhileWithIndex
Из SO берутся элементы до тех пор, пока функция, переданная в качестве параметра, возвращает true. Отличие от takeWhile
в том, что еще одним параметром, переданным в функцию, является индекс сгенерированного элемента.
Консоль:
debounce
Из SO берутся лишь элементы, после которых не было новых элементов N секунд.
Консоль:
В данном примере элементы генерируются c разными задержками. Поэтому debounce
сработает всего несколько раз в момент, когда между элементами будет достаточный временной промежуток.
Трансформация
buffer
Элементы из SO по определенным правилам объединяются в массивы и генерируются в RO. В качестве параметров передаются count
(максимальное число элементов в массиве) и timeSpan
(время максимального ожидания наполнения текущего массива из элементов SO). Таким образом, элемент RO являет собой массив [T] длиной от 0 до count
.
Консоль:
Максимальное число элементов в массиве указано равное трем, и оно никак не влияет на наполнение массива в данном примере. За максимальное время ожидания наполнения таймер успевает сгенерировать не более 2 элементов. Однако период таймера и время ожидания наполнения выбраны таким образом, что третий массив успеет получить лишь один объект.
flatMap
Каждый элемент SO превращается в отдельный Observable
, и все элементы из [O1, O2, O3…] объединяются в RO. Порядок генерации элементов в RO зависит от времени их генерации в исходных [O1, O2, O3…] (как в команде merge
).
Консоль:
flatMapFirst
Каждый элемент SO превращается в отдельный Observable
.
1) Изначально подписываемся на O1, его элементы зеркально генерируются в RO. Пока O1 генерирует элементы, все последующие Observable
, сгенерированные из SO, отбрасываются, на них не подписываемся.
2) как только O1 оканчивается, если будет сгенерирован новый Observable
, на него подпишутся, и его элементы будут дублироваться в RO.
Повторяем пункт 1, но вместо O1 берем последний сгенерированный Observable
.
Консоль:
В примере благодаря задержкам в генерации мы видим, что, пока не произойдет окончание первого Observable, никакой новый Observable его не заменит.
flatMapLatest
Каждый элемент SO превращается в отдельный Observable
. Изначально подписываемся на O1, его элементы зеркально генерируются в RO. Как только из SO выпускается очередной элемент и на его основе генерируется очередной Observable
, элементы предыдущего Observable
отбрасываются, т.к. происходит отписка. Таким образом в RO — элементы из последнего генерированного Observable
.
Консоль:
В примере благодаря задержкам в генерации мы видим, что, как только генерируется новый Observable
, происходит отписка от предыдущего Observable
.
flatMapWithIndex
Каждый элемент SO превращается в отдельный Observable
, и все элементы из [O1, O2, O3…] объединяются в RO. Порядок генерации элементов в RO зависит от времени их генерации в исходных [O1, O2, O3…] (как в команде merge
). Отличие от flatMap
в том, что еще одним параметром, переданным в функцию, является индекс сгенерированного элемента.
Консоль:
map
Элементы SO преобразуются, не меняя порядок их генерации. Можно менять не только значение, но и тип элементов.
Консоль:
mapWithIndex
Элементы SO преобразуются, не меняя порядок их генерации. Можно менять не только значение, но и тип элементов. Отличие от map в том, что еще одним параметром, переданным в функцию, является индекс сгенерированного элемента.
Консоль:
window
Элементы из SO по определенным правилам передаются в генерирующиеся новые Observable
. В качестве параметров передаются count
(максимальное число элементов, которые будут сгенерированы каждым Observable
) и timeSpan
(время максимального ожидания наполнения текущего Observable
из элементов SO). Таким образом элемент RO являет собой Observable
, число генерируемых элементов которого равно от 0 до N. Основное отличие от bufffer
в том, что элементы SO зеркалятся сгенерированными Observable
моментально, а в случае buffer
мы вынуждены ждать указанное в качестве параметра максимальное время (если буфер не заполнится раньше).
Консоль:
В примере используются временные задержки, что помогает добиться частичной наполненности генерируемых Observable
.
Операторы математические и агрегирования
reduce
Каждый элемент SO преобразуется с помощью переданной функции, результат операции передается в качестве параметра в функцию на следующем шаге. Как только SO генерирует терминальное состояние, RO генерирует результат, т.е. RO сгенерирует лишь один элемент.
Консоль:
scan
Каждый элемент SO преобразуется с помощью переданной функции, результат операции генерируется в RO, но, кроме этого, оно передается в качестве параметра в функцию на следующем шаге. В отличии от reduce число элементов в RO равно числу элементов в SO.
Консоль:
toArray
Все элементы из SO после генерации терминального состояния объединяются в массив, и генерируются RO.
Консоль:
Работа с ошибками
catchError
Позволяет перехватить сгенерированную ошибку из SO и заменить ее на новый Observable
, который теперь будет генерировать элементы.
Консоль:
После генерации очередного элемента была сгенерирована ошибка, но мы её перехватили и вернули взамен новый Observable
.
catchErrorJustReturn
Позволяет перехватить сгенерированную ошибку из SO и заменить её на указанный элемент, после этого SO генерирует Completed
.
Консоль:
После генерации очередного элемента была сгенерирована ошибка, но мы её перехватили и вернули взамен новый элемент.
retry
Позволяет перехватить сгенерированную ошибку из SO и в зависимости от переданного параметра попытаться запустить SO c начала нужное число раз в надежде, что ошибка не повторится.
Консоль:
Передаваемое в оператор целое число означает количество попыток дождаться успешного окончания. 0 — ни одной попытки, т.е. цепочка ни разу не будет запущена, и просто произойдет completed событие. 1 — такое же поведение, как будто оператор и не не был применен. 2 — исходная попытка + дополнительная, и т.д. Если в оператор не передать параметр, то количество попыток повторить будет бесконечным.
retryWhen
Позволяет перехватить сгенерированную ошибку из SO, и в зависимости от типа ошибки мы либо повторно генерируем ошибку, которая пробрасывается в RO, и на этом выполнение заканчивается, либо генерируем Observable
(tryObservable
), генерация каждого корректного элемента которого выполнит повторную подписку на SO в надежде, что ошибка исчезнет. Если tryObservable
заканчивается ошибкой, она пробрасывается в RO, и на этом выполнение заканчивается.
Консоль:
Я встроил инкремент переменной i в генерацию sequenceWithError
, чтобы на 3й попытке ошибка исчезла. Если раскоментировать генерацию ошибки RxError.Overflow
, мы её не перехватим в операторе retryWhen
и пробросим в RO.
Операторы для работы с Connectable Observable
multicast
Позволяет проксировать элементы из исходной SO на Subject
, переданный в качестве параметра. Подписываться нужно именно на этот Subject
, генерация элементов Subject
начнется после вызова оператора connect
.
Консоль:
publish
publish = multicast + replay subject
Позволяет создавать Connectable Observable
, которые не генерируют события даже после subscribe
. Для старта генерации таким Observable
нужно дать команду connect
. Это позволяет подписать несколько Observer
к одному Observable
и начать генерировать элементы одновременно, вне зависимости от того, когда был выполнен subscribe
.
Консоль:
Как видно, хоть подписка и была произведена в разное время, пока не вызвали команду connect — генерация элементов не началась. Зато благодаря команде debug видно, что даже после того как все отписались, последовательность продолжила генерировать элементы.
refCount
Позволяет создать обычный Observable
из Connectable
. После первого вызова subscribe
к этому обычному Observable
происходит подписка Connectable
на SO.
Получается что-то вроде
SO будет продолжать генерировать элементы до тех пор, пока есть хотя бы один подписанный на refCountSequence
. Как только все подписки на refCountSequence
аннулируются, происходит отписка и publishSequence
от SO.
Консоль:
replay
Если SO обычный, — конвертирует его в Connectable
. После этого все, кто подпишутся на него после вызова connect()
, мгновенно получат в качестве первых элементов последние сгенерированные N элементов. Даже если отпишутся все — Connectable
будет продолжать генерировать элементы.
Консоль:
replayAll
Если SO обычный, — конвертирует его в Connectable
. Все, кто подпишутся на него, после вызова connect()
получат сначала все элементы, которые были сгенерированы ранее. Даже если отпишутся все, Connectable
будет продолжать генерировать элементы.
Консоль:
Вспомогательные методы
debug
RO полностью дублирует SO, но логируются все события с временной меткой.
Консоль:
do
RO полностью дублирует SO, но мы встраиваем перехватчик всех событий из жизненного цикла SO.
Консоль:
delaySubscription
Дублирует элементы из SO в RO, но с временной задержкой, указанной в качестве параметра.
Консоль:
observeOn
Указывает, на каком Scheduler
должен выполнять свою работу Observer
, особенно критично при работе с GUI.
Консоль:
Как видно, благодаря observeOn
мы смогли выполнить код внутри subscribe
на другом потоке, хотя оба Observable
были запущены на background
.
subscribe
Оператор, связывающий Observable
с Observer
, позволяет подписаться на все события из Observable
.
Консоль:
subscribeOn
Указывает, на каком Scheduler
выполнять подписку на Observable
. Редко используемый оператор. Для получения колбэков на нужном Scheduler
следует пользоваться observeOn
.
Консоль:
timeout
Дублирует элементы из SO в RO, но если в течение указанного времени SO не сгенерировало ни одного элемента, RO генерирует ошибку.
Консоль:
using
using
Позволяет проинструктировать Observable
создать ресурс, который будет жить лишь пока жив RO, в качестве параметров передаются 2 фабрики, одна генерирует ресурс, вторая — Observable
из ресурса, у которых будет единое время жизни.
Консоль:
Как видно, после того, как Observable
закончил генерировать элементы, у нашего ресурса Factory
был вызван метод dispose
.
За материал выражаем благодарность международной IT-компании Noveo.
Добавить комментарий