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

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
12-Авг-2020 13:46

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

===========
Источник:
habr.com
===========

===========
Автор оригинала: Zafar Ivaev
===========
Похожие новости: Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_swift, #_programming, #_swift, #_ios, #_mobile, #_xcode, #_blog_kompanii_otus._onlajnobrazovanie (
Блог компании OTUS. Онлайн-образование
)
, #_razrabotka_pod_ios (
Разработка под iOS
)
, #_swift
Профиль  ЛС 
Показать сообщения:     

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

Текущее время: 25-Ноя 15:09
Часовой пояс: UTC + 5