В этой статье мы рассмотрим шесть полезных операторов объединения в Combine. Мы сделаем это на примерах, экспериментируя с каждым из них в Xcode Playground.
Исходный код доступен в конце статьи.
Ну что ж, без лишних разглагольствований, давайте приступим.
1. prepend
Эта группа операторов позволяет нам добавлять (prepend дословно добавить в начало) к нашему исходному паблишеру события, значения или других паблишеров:
import Foundationimport Combinevar 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? Не стесняйтесь посещать мои другие статьи: