Преобразование, фильтрация, планирование

Как мы говорили в предыдущей статье, Combine состоит из асинхронных потоков данных, исходящих как выходы из Publishers, которые обрабатываются как входы Subscribers, которые получают их через Subscription, отслеживая изменения.

Одной из наиболее важных деталей этого является то, что Publisher и Subscriber должны быть всегда совместимы друг с другом, что означает, что Output связанный тип из Publisher должен быть тем же самым Input связанным типом из Subscriber . Объединение этих потоков похоже на игру с головоломкой: чтобы соединить две части, они должны совпасть.

Вы, должно быть, думаете: хорошо, в этом случае мне просто нужен подписчик, который работает с входными значениями, просто преобразуя их в любой другой тип, который нам нужен, и все в порядке! Хорошо, дорогие читатели, это, безусловно, работает, но, как мы говорили ранее, основная идея Combine заключается в том, чтобы работать со всем, используя реактивную и декларативную парадигму, без какой-либо императивной команды, которая увеличила бы сложность наших задач.

Мы хотим избежать любых традиционных строк кода, которые присваивают переменные, выполняют операторы if then else, а также любые циклы и другие проверки. «Хорошо, но как мы могли это сделать?». Комбинат существует именно для этого, в нем есть что-то волшебное, что мы любим называть Operators .

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

Определение операторов объединения

По сути, операторы похожи на функции, которые принимают одно или несколько Publishers в качестве входных данных и возвращают новый новый, содержащий точное поведение, которое мы хотим. Например, если наш издатель испускает поток integers, а у нас есть Subscriber, который ожидает плавающее число или, может быть, строку, у нас может быть оператор, ответственный за преобразование его выходных данных в ожидаемый тип. Если мы хотим обрабатывать только некоторые определенные выходные данные, мы можем положиться на оператор, который выбирает только те выходные данные, которые нам действительно нужны.

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

Итак, что на самом деле происходит, так это то, что у нас есть исходный издатель, который в нашей литературе называется вышестоящим издателем (все, что вводится в качестве входных данных для оператора), который публикует некоторые значения и передает их через оператор, который преобразует их в нижестоящего издателя. который публикует значения того же типа, что и тип, связанный с подписчиком Input, и добавляет к нему любое дополнительное поведение. При этом у нас есть идеальный конвейер, который доставляет значения из источника (Publisher) в пункт назначения (Subscriber) в правильном формате.

Я хотел бы просто прояснить, что оператор вообще не изменяет значения, поскольку вы можете проводить некоторые сравнения с операторами Swift Arrays. Операторы Combine просто преобразуют вышестоящего издателя в новый, который фактически публикует тип значений, который нам нужен, с любым дополнительным поведением, вот и все.

Типы операторов

Как мы говорили выше, операторы Combine могут выполнять множество различных задач, которые мы прекрасно могли бы выполнять с помощью императивных команд, но, как мы традиционно делали с методами, позвольте мне предоставить вам каждый тип оператора, который у нас будет:

  • Операторы преобразования: создайте новый объект Publisher, который берет выходные данные восходящего потока и преобразует их в новые значения, которые будут опубликованы нижестоящими. Мы могли бы сказать, что длина нашего вывода одинакова
  • Операторы фильтрации: создайте новый Publisher, который принимает только некоторые значения, опубликованные вышестоящими. Мы могли бы сказать, что длина нашего нижнего течения меньше, чем длина нашего верхнего течения.
  • Объединяющие операторы: берут двух или более издателей и выдают некоторые значения на основе выходных данных вышестоящего издателя.
  • Операторы синхронизации: отвечают за доставку значений от вышестоящего издателя только при проверке некоторых условий.
  • Операторы сложения: создайте нового издателя, который выдает дополнительные выходные данные, помимо исходных.
  • Операторы метаданных: возвращают выходные данные с некоторой информацией о вышестоящем издателе.
  • Операторы планирования: контролируйте время отправки значений из восходящего потока.

Рассмотрим подробно каждый тип на примере HoldValue издателя, реализованного нами в предыдущей статье.

Операторы преобразования

1. Карта

Когда каждое значение публикуется из восходящего потока, это значение преобразуется в новое нового типа (или, возможно, такое же). Аналогично оператору карты Array:

Основной вариант использования: преобразование моделей данных.

2. Собрать

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

Обратите внимание только на одну деталь: вы можете подумать: «Эй, а когда оператор на самом деле собирает эти восходящие значения?». Ответ логичен: как раз тогда, когда вышестоящий издатель выдает завершающее событие.

Основной вариант использования: объединение всех опубликованных значений в один объект.

3. Плоская карта

Этот оператор может быть одним из самых сложных из всех. Идея flatMap в основном состоит в том, чтобы брать выходные данные из восходящей ветки и отображать их таким образом, чтобы они трансформировались в другого издателя, созданного из предыдущего вывода.

То, что происходит здесь в следующем примере, похоже на то, когда у нас есть массив массивов. Представьте, что у вас есть некоторый тип, который работает как вывод для пользовательского издателя, и внутри этого типа есть еще один издатель. Что делает оператор flatMap, так это преобразует исходный издатель, который имеет этот тип структуры в качестве вывода, в тот, который фактически выдает значения внутреннего публикатора. Смущенный? Проверьте изображение ниже:

Исправьте, что у нас есть вышестоящий издатель, который выдает некоторые значения, содержащие внутри другого издателя, который также публикует некоторые значения. Что мы делаем, так это создаем нового издателя, который публикует значения из внутреннего издателя из только что опубликованного значения:

Мы создаем структуру S, содержащую издателя, а затем дважды создаем ее экземпляр.

После этого мы создаем новую тему с S в качестве типа вывода. Сначала мы публикуем наш первый экземпляр S, затем заставляем его внутреннего издателя публиковать два значения, 1 и 2 соответственно. Получившийся в результате издатель из flatMap опубликует два значения в качестве выходных данных, так как он фактически полагается на внутреннего издателя.

После этого мы публикуем второй экземпляр S и заставляем его внутреннего издателя выдавать два новых значения, которые должны быть опубликованы внешним субъектом. Большой! Теперь мы сопоставляем нашего издателя с внутренним и выдаем его значения независимо от вывода внешнего издателя.

Наиболее распространенный вариант использования: мы делаем два последовательных вызова API, второй выполняется с выводом первого в качестве параметра.

4 . ЗаменитьNil

Иногда наши издатели могут выдавать нулевые значения, а нашим подписчикам необходимо обрабатывать существующие входные данные. ReplaceNil — это надежное решение для преобразования nil в допустимый ввод, точно так же, как мы имели дело с nil-coalescing :

5. Компактная карта

Делает то же самое, что и map , но также работает как оператор фильтрации, поскольку игнорирует все значения nil:

6. Сканировать

Этот оператор работает очень похоже на операцию reduce, с которой мы имеем дело Arrays. Каждый раз, когда мы публикуем новое значение, это значение работает с накопленным результатом предыдущих выходов, и выдается новое значение. Чтобы использовать этот оператор, нам нужно передать начальное накопленное значение и бинарное закрытие:

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

Основной вариант использования: мы хотим опубликовать общий объем продаж магазина, когда он продает новый товар.

6. Уменьшить

Аналогично scan, но мы не выдаем никаких промежуточных значений. Вместо этого мы просто публикуем окончательное накопленное значение. Он публикуется только после завершения конвейера восходящего потока. Поскольку на самом деле у нас нет способа отправить завершение finish в нашем пользовательском издателе, мы полагаемся на PassthroughSubject в следующем примере:

Вариант использования: мы всего лишь вычисляем накопленные продажи к концу дня.

Операторы фильтрации

1. Фильтр

Берет вышестоящего издателя и публикует только его выходные значения, следующие за некоторым предикатом:

2 . УдалитьДубликаты

Этот оператор публикует новое значение только в том случае, если оно не совпадает с предыдущим из восходящего потока.

Важно напомнить вам, что он не удаляет дубликаты, которые не находятся непосредственно перед текущим элементом. Как видите, 1 печатается дважды, так как он выдается только в конце после того, как стал первым элементом. Он удаляет последовательные дубликаты.

Пример использования: вы слушаете текстовое поле, которое не хотите публиковать тексты, которые сразу равны

3. Игнорирует вывод

Этот оператор отбрасывает все выходные значения вышестоящего издателя, генерируя только событие завершения.

4. Первый(где:)

Этот оператор заставляет нисходящий поток выдавать только первое выходное значение из восходящего потока, которое соответствует некоторому условию.

5. Первый

Нисходящий поток публикует только первый элемент из восходящего потока без каких-либо особых условий.

6. Последний(где:)

Та же логика, что и First(where:) , но вы берете последний опубликованный элемент, следуя этому условию, и он генерируется только после завершения восходящего потока.

7. Последний

Та же логика, что и в First, но вы берете последний элемент из потока после завершения восходящего потока.

8. DropFirst и варианты

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

8.1. Отбросьте (пока :)

Отбрасывает первые элементы, которые следуют некоторому условию, пока не будет найден элемент, который больше не следует этому предикату.

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

8.2. DropFirst (количество:)

Отбросьте первые n элементов, переданных как количество.

8.3. Отбросить (до выхода из)

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

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

9. Префикс

Я не буду приводить примеры prefix, так как единственное, что я должен сказать, это то, что это полная противоположность drop. Вместо того, чтобы отбрасывать первую часть опубликованных значений из восходящего потока, мы рассматриваем только их:

Объединение операторов

Хорошо, я знаю, что это имя кажется избыточным, но этот тип операторов делает то, чего не делали предыдущие: он объединяет несколько вышестоящих издателей в один нижестоящий. Иногда у вас могут быть разные источники асинхронных данных, и вам действительно нужен способ объединения их результатов в одном потоке. Фраза, которая резюмирует эту идею, звучит так: я просто хочу результатов, откуда бы они ни исходили.

1. Почтовый индекс

Этот оператор принимает двух или более издателей и возвращает их выходные данные в виде кортежа, что означает, что новое значение публикуется нижестоящим, только если все вышестоящие издатели уже опубликовали значение.

Предположим, у нас есть три восходящих издателя HoldValue. Каждый из них публикует значение за раз, но нижестоящий будет публиковать новый кортеж только после того, как все они опубликуют свой первый вывод:

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

Вариант использования: представьте, что вы собираетесь выполнить какую-то операцию, которая использует камеру, звук, а также вашу библиотеку фотографий с iPhone. В этом контексте вы собираетесь запрашивать авторизацию для всех этих ресурсов. Поскольку вы имеете право на доступ к функции только после предоставления всех разрешений, zip издатель, сочетающий статус авторизации из трех источников, будет уместен.

2. ОбъединитьПоследние

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

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

Пример использования: представьте, что у вас есть несколько издателей, которые отслеживают значения из текстовых полей. Вы хотите объединить их все, чтобы получить некоторый вывод в виде кортежа всего содержимого текстовых полей. Если вы измените один из них, вывод должен быть сверен с текущим содержимым других, так как нас интересуют текущие значения.

3. Объединить

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

Вариант использования: представьте, что вы привлекаете в свое приложение некоторых пользователей, которые могут поступать из разных API. Все извлекаемые объекты имеют одинаковый формат, поэтому вы можете декодировать их все в одну и ту же модель данных Decodable. Каждый пользователь является выходом издателя. Поскольку все пользователи имеют одно и то же значение, независимо от того, откуда они пришли, мы могли бы идеально объединить всех издателей в Merge, который к концу помещает пользователей в своего рода единый массив.

4. переключаться на последние

Этот оператор в основном сглаживает издателя издателей, которые мы хотим отслеживать, значения, опубликованные последним отправленным издателем. Взгляните на этот код:

Subject — это издатель, который публикует PassthroughSubject издателей, которые выдают целые числа. Когда мы применяем switchToLatest , нас на самом деле интересуют значения, опубликованные последним издателем, который был выпущен subject . Когда мы отправляем s1 , нас интересуют только значения, опубликованные s1 , любое значение, выпущенное каким-либо другим издателем, будет отброшено.

Вариант использования: представьте, что у вас есть панель вкладок с тремя экранами, каждый из которых соответствует вкладке. У каждого из этих viewController есть издатель HoldValue, который продолжает генерировать значения, но мы хотим отслеживать только те значения, которые были опубликованы на выбранной вкладке. Мы должны отображать значения в небольшом предупреждении перед UITabBarController . Наиболее подходящим решением было бы полагаться на издателя, который выдает издателя из соответствующего ViewController, как только он появляется, а затем с помощью оператора switchLatest мы отслеживаем значения, поступающие с выбранного экрана.

Дополнительные издатели

Этот тип издателя означает добавление новых значений помимо исходных.

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

1. добавить

Добавляет начальное значение перед всеми опубликованными значениями

2. добавить

Добавьте новые значения в конце восходящего потока после его завершения.

Операторы метаданных

1. Считать

Возвращает, сколько значений было опубликовано вверх. Выдает значение только после завершения восходящего потока.

2. Выход (в:)

Публикует элемент в заданной позиции, как только он существует:

3. Выход (в:)

Публикует все значения, опубликованные восходящим потоком в заданном диапазоне.

4. содержит

Нисходящий поток публикует логическое значение, указывающее, содержат ли значения восходящего потока какое-то конкретное значение или есть какое-то значение, следующее за определенным condition(contains(where:)).

5. всеудовлетворить

Публикует логическое значение, определяющее, соответствуют ли все значения из восходящего потока некоторому условию. Публикуется только после завершения восходящего потока.

Планирование издателей

1. Задержка

Начинает публиковать значения из восходящего потока (или события завершения) только после некоторой задержки

В этом примере мы сообщаем нашему нисходящему издателю, чтобы он выдавал восходящие значения только через 2 секунды, отсчитываемые в основном потоке.

2. Отказаться

Этот оператор применим, когда мы не хотим, чтобы события генерировались в слишком близком временном интервале. На этот раз давайте рассмотрим пример пользовательского интерфейса: представьте, что у вас есть сцена SwiftUI, в которой вы отображаете TextField, выровненный по вертикали с меткой Text. Мы полагаемся на класс ContentViewModel как StateObject для нашего ContentView и два опубликованных свойства внутри: строку firstName, привязку которой мы вводим в наш TextField, и label, который будет отображаться на Text:

Поскольку мы структурировали наш поток Combine, каждый раз, когда издатель с оболочкой firstName выдает новую строку, она будет назначена нашему содержимому label, которое также оборачивает другой издатель, который при обновлении отображает новый текст для нашей метки Text в нашей сцене SwiftUI.

Исправьте, что наш Text обновляется сразу после ввода нового контента в наш TextField , так как мы слушаем нашего firstName издателя.

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

Вот наш результат:

Последние изменения публикуются сразу после того, как прошла одна секунда.

3. Дроссель

Этот оператор заставляет нисходящий поток публиковать один элемент за определенный интервал времени. Например, если бы мы применили это в нашем предыдущем примере, наш издатель label генерировал бы одно событие в секунду, будучи первым элементом из восходящего потока или последним (вы можете определить). Если новое значение не публикуется, последнее значение повторно публикуется в нисходящем направлении:

Заключение

Поэтому я приложил огромные усилия, чтобы перечислить все операторы Combine, которые будут важны при построении асинхронного потока для передачи данных между разными потоками.

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

При всем этом Combine становится одним из самых мощных механизмов для передачи данных между несколькими объектами, оставляя делегирование, замыкания, центры уведомлений и RxSwift устаревшими (только мое мнение!).

Я надеюсь, что это помогло вам лучше понять комбинированное и реактивное программирование и применить эти концепции в ваших собственных мобильных проектах.