Перевод статьи подготовлен в преддверии старта продвинутого курса «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? Не стесняйтесь посещать мои другие статьи: