6 объединяющих операторов Swift Combine, которые вам следует знать

Перевод статьи подготовлен в преддверии старта продвинутого курса «iOS-Разработчик».


В этой статье мы рассмотрим шесть полезных операторов объединения в Combine. Мы сделаем это на примерах, экспериментируя с каждым из них в Xcode Playground.

Исходный код доступен в конце статьи.

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

1. prepend

Эта группа операторов позволяет нам добавлять (prepend — дословно “добавить в начало”) к нашему исходному паблишеру события, значения или других паблишеров:

import Foundation import Combine  var subscriptions = Set<AnyCancellable>()  func prependOutputExample() {     let stringPublisher = ["World!"].publisher          stringPublisher         .prepend("Hello")         .sink(receiveValue: { print($0) })         .store(in: &subscriptions) }

Результат: Hello и World! выводятся в последовательном порядке:

Теперь давайте добавим другого издателя того же типа:

func prependPublisherExample() {     let subject = PassthroughSubject<String, Never>()     let stringPublisher = ["Break things!"].publisher          stringPublisher         .prepend(subject)         .sink(receiveValue: { print($0) })         .store(in: &subscriptions)          subject.send("Run code")     subject.send(completion: .finished) }

Результат аналогичен предыдущему (обратите внимание, что нам нужно отправить событие .finished в subject, чтобы оператор .prepend работал):

2. append

Оператор .append (дословно “добавить в конец”) работает аналогично .prepend, но в этом случае мы добавляем значения к исходному паблишеру:

func appendOutputExample() {     let stringPublisher = ["Hello"].publisher          stringPublisher         .append("World!")         .sink(receiveValue: { print($0) })         .store(in: &subscriptions) }

В результате мы видим Hello и World! выведенные на консоли:

Аналогично тому, как ранее мы использовали .prepend для добавления другого Publisherа, у нас также есть такая возможность и для оператора .append:

3. switchToLatest

Более сложный оператор .switchToLatest позволяет нам объединить серию паблишеров в один поток событий:

func switchToLatestExample() {     let stringSubject1 = PassthroughSubject<String, Never>()     let stringSubject2 = PassthroughSubject<String, Never>()     let stringSubject3 = PassthroughSubject<String, Never>()          let subjects = PassthroughSubject<PassthroughSubject<String, Never>, Never>()          subjects         .switchToLatest()         .sink(receiveValue: { print($0) })         .store(in: &subscriptions)          subjects.send(stringSubject1)          stringSubject1.send("A")          subjects.send(stringSubject2)          stringSubject1.send("B") // отброшено          stringSubject2.send("C")     stringSubject2.send("D")          subjects.send(stringSubject3)          stringSubject2.send("E") // отброшено     stringSubject2.send("F") // отброшено          stringSubject3.send("G")          stringSubject3.send(completion: .finished) } 

Вот что происходит в коде:

  • Мы создаем три объекта PassthroughSubject, которым мы будем отправлять значения.
  • Мы создаем главный объект PassthroughSubject, который отправляет другие объекты PassthroughSubject.
  • Мы отправляем stringSubject1 на основной subject.
  • stringSubject1 получает значение A.
  • Мы отправляем stringSubject2 на основной subject, автоматически отбрасывая события stringSubject1.
  • Точно так же мы отправляем значения в stringSubject2, подключаемся к stringSubject3 и отправляем ему событие завершения.

В результате мы видим вывод A, C, D и G:

Для простоты, функция isAvailable возвращает случайное значение Bool после некоторой задержки.

func switchToLatestExample2() {     func isAvailable(query: String) -> Future<Bool, Never> {         return Future { promise in             DispatchQueue.main.asyncAfter(deadline: .now() + 2) {                 promise(.success(Bool.random()))             }         }     }          let searchSubject = PassthroughSubject<String, Never>()          searchSubject         .print("subject")         .map { isAvailable(query: $0) }         .print("search")         .switchToLatest()         .sink(receiveValue: { print($0) })         .store(in: &subscriptions)          searchSubject.send("Query 1")     DispatchQueue.main.asyncAfter(deadline: .now() + 1) {         searchSubject.send( "Query 2")     } }

Благодаря оператору .switchToLatest мы достигаем того, чего хотим. Только одно значение Bool будет выведено на экран:

4. merge(with:)

Мы используем .merge(with:) для объединения двух Publishersов, как если бы мы получали значения только от одного:

func mergeWithExample() {     let stringSubject1 = PassthroughSubject<String, Never>()     let stringSubject2 = PassthroughSubject<String, Never>()          stringSubject1         .merge(with: stringSubject2)         .sink(receiveValue: { print($0) })         .store(in: &subscriptions)          stringSubject1.send("A")          stringSubject2.send("B")          stringSubject2.send("C")          stringSubject1.send("D") } 

Результатом является чередующаяся последовательность элементов:

5. combineLatest

Оператор .combineLatest паблишит кортеж, содержащий последнее значение каждого издателя.

Чтобы проиллюстрировать это, рассмотрим следующий реальный пример: у нас есть имя пользователя, пароль UITextFields и кнопка продолжения. Мы хотим держать кнопку отключенной до тех пор, пока имя пользователя не будет содержать не менее пяти символов, а пароль — не менее восьми. Мы можем легко добиться этого, используя оператор .combineLatest:

func combineLatestExample() {     let usernameTextField = CurrentValueSubject<String, Never>("")     let passwordTextField = CurrentValueSubject<String, Never>("")          let isButtonEnabled = CurrentValueSubject<Bool, Never>(false)          usernameTextField         .combineLatest(passwordTextField)         .handleEvents(receiveOutput: { (username, password) in             print("Username: \(username), password: \(password)")             let isSatisfied = username.count >= 5 && password.count >= 8             isButtonEnabled.send(isSatisfied)         })         .sink(receiveValue: { _ in })         .store(in: &subscriptions)          isButtonEnabled         .sink { print("isButtonEnabled: \($0)") }         .store(in: &subscriptions)          usernameTextField.send("user")     usernameTextField.send("user12")          passwordTextField.send("12")     passwordTextField.send("12345678") }

После того, как usernameTextField и passwordTextField получат user12 и 12345678 соответственно, условие удовлетворяется, и кнопка активируется:

6. zip

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

func zipExample() {     let intSubject1 = PassthroughSubject<Int, Never>()     let intSubject2 = PassthroughSubject<Int, Never>()          let foundIdenticalPairSubject = PassthroughSubject<Bool, Never>()          intSubject1         .zip(intSubject2)         .handleEvents(receiveOutput: { (value1, value2) in             print("value1: \(value1), value2: \(value2)")             let isIdentical = value1 == value2             foundIdenticalPairSubject.send(isIdentical)         })         .sink(receiveValue: { _ in })         .store(in: &subscriptions)          foundIdenticalPairSubject         .sink(receiveValue: { print("is identical: \($0)") })         .store(in: &subscriptions)          intSubject1.send(0)     intSubject1.send(1)          intSubject2.send(4)          intSubject1.send(6)     intSubject2.send(1)     intSubject2.send(7)          intSubject2.send(9) // Не отображено, потому что его пара еще не отправлена }

У нас есть следующие соответствующие значения из intSubject1 и intSubject2:

  • 0 и 4
  • 1 и 1
  • 6 и 7

Последние значение 9 не выводится, поскольку intSubject1 еще не опубликовал соответствующее значение:

Ресурсы

Исходный код доступен на Gist.

Заключение

Вас интересуют другие типы операторов Combine? Не стесняйтесь посещать мои другие статьи:

ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/514848/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *