В этой статье мы рассмотрим шесть полезных операторов объединения в 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/