Добрый день! Я — Иваев Зафар, iOS разработчик в компании Usetech. В этой статье мы узнаем как фреймворк Combine помогает нам разрабатывать функционал приложения с помощью встроенных функций — операторов. Итак, мы покроем следующие типы операторов:
Последовательные операторы
Объединяющие операторы
Последовательные операторы
.first
,.first(where:)
.last
,.last(where:)
.output(at:)
,.output(in:)
.count
.contains
,.contains(where:)
.allSatisfy
.reduce
1. first
Оператор .first
позволяет нам получить первый элемент из последовательности:
import Foundation
import Combine
var subscriptions = Set<AnyCancellable>()
func firstExample() {
let intPublisher = [10, 20, 100, 200].publisher
intPublisher
.first()
.sink(receiveValue: { print("First: \($0)") })
.store(in: &subscriptions)
}
В результате, 10 принтится в консоли:
![](https://habrastorage.org/getpro/habr/upload_files/e3d/ee3/348/e3dee3348f8b97269996a523186bbf81.png)
Мы так же можем указать предикат, используя .first(where)
версию оператора:
func firstWhereExample() {
let intPublisher = [23, 33, 50, 27, 101, 108].publisher
intPublisher
.first(where: { $0.isMultiple(of: 2) })
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
Как и ожидалось, консоль выводит значение 50:
![](https://habrastorage.org/getpro/habr/upload_files/91e/d9a/413/91ed9a41391f77360ea7b69d842d98fd.png)
2. last
Так же, как мы получили первый элемент последовательности, мы можем получить и последний:
func lastExample() {
let intPublisher = [10, 20, 100, 200].publisher
intPublisher
.last()
.sink(receiveValue: { print("Last: \($0)") })
.store(in: &subscriptions)
}
![](https://habrastorage.org/getpro/habr/upload_files/973/fe7/c75/973fe7c75c476ea2c60fa5900b23465b.png)
Точно так же мы можем предоставить условие, используя .last(where:)
вариант:
func lastWhereExample() {
let intPublisher = [23, 33, 50, 27, 101, 108].publisher
intPublisher
.last(where: { $0.isMultiple(of: 2) })
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
Мы видим, что значение 108 было выведено в консоли, поскольку это последний элемент, удовлетворяющий предикату:
![](https://habrastorage.org/getpro/habr/upload_files/ca7/1e3/c97/ca71e3c97e46ca167016d225fa484824.png)
3. output
Версия этого оператора, .output(at:)
, получает определенный элемент по указанному индексу:
func outputAtExample() {
let stringPublisher = ["A", "B", "C", "D"].publisher
stringPublisher
.output(at: 3)
.sink(receiveValue: { print("Output: \($0)") })
.store(in: &subscriptions)
}
Индекс - 3. Следовательно, выводится буква «D»:
![](https://habrastorage.org/getpro/habr/upload_files/7de/d3f/b8f/7ded3fb8f54ea29f204ca4161fb9f5f3.png)
Мы можем получить все элементы, принадлежащие указанному диапазону, используя версию .output(in:)
:
![](https://habrastorage.org/getpro/habr/upload_files/609/bba/73e/609bba73e83a6c9511bb8e0b1b7051c9.png)
4. count
Как и его аналог из стандартной библиотеки Swift, оператор .count
возвращает количество опубликованных значений последовательности:
func countExample() {
let voidSubject = PassthroughSubject<Void, Never>()
voidSubject
.count()
.sink(receiveValue: { print("Total \($0) events")})
.store(in: &subscriptions)
voidSubject.send()
voidSubject.send()
voidSubject.send()
voidSubject.send(completion: .finished)
}
Как мы видим, мы отправили три события Void
, поэтому в консоли было выведено 3 события:
![](https://habrastorage.org/getpro/habr/upload_files/889/99b/eaf/88999beafee06090f7f557936893d654.png)
5. contains
Оператор .contains
возвращает true
или false
в зависимости от того, был ли найден конкретный элемент в последовательности:
func containsExample() {
let letterPublisher = ["A", "B", "C", "D", "E"].publisher
letterPublisher
.contains("Z")
.sink(receiveValue: {
print("Does contain the specified character: \($0)")
})
.store(in: &subscriptions)
}
Здесь мы ищем букву «Z». Так как она не была найдена, получаем false
:
![](https://habrastorage.org/getpro/habr/upload_files/cc6/794/f6b/cc6794f6b532437494ebc987151f448c.png)
Мы можем предоставить предикат, используя ковариант .contains(where:)
:
func containsWhereExample() {
let letterPublisher = ["a", "b", "C", "d", "e"].publisher
letterPublisher
.contains(where: { $0.first!.isUppercase })
.sink(receiveValue: {
print("Does contain an uppercase character: \($0)")
})
.store(in: &subscriptions)
}
Так как letterPublisher
выпускает заглавную букву «C», в консоли выводится true
:
![](https://habrastorage.org/getpro/habr/upload_files/e21/add/c4d/e21addc4d8304d59c00697ebdd3b9055.png)
6. allSatisfy
Подобно предыдущему оператору .contains, оператор .allSatisfy
возвращает значение типа Bool
. Однако он возвращает true
только в том случае, если каждый отдельный элемент удовлетворяет предоставленному условию:
func allSatisfyExample() {
let intPublisher = [3, 9, 27, 81, 244].publisher
intPublisher
.allSatisfy({ $0.isMultiple(of: 3) })
.sink(receiveValue: {
print("All numbers are divisible by 3: \($0)")
})
.store(in: &subscriptions)
}
В этом случае условию удовлетворяют все элементы, кроме одного. Следовательно, получаем false
:
![](https://habrastorage.org/getpro/habr/upload_files/2c9/f05/7ab/2c9f057ab51706b996030083ff640fe7.png)
7. reduce
Последний последовательный оператор, .reduce
, предоставляет мощный механизм для накопления элементов последовательности и возврата окончательного значения по завершении:
func reduceExample() {
let intPublisher = [3, 9, 27, 81, 244].publisher
intPublisher
.reduce(0) { accumulated, value in accumulated + value }
.sink(receiveValue: { print("Sum: \($0)") })
.store(in: &subscriptions)
}
Здесь мы вычисляем сумму всех элементов. Накопление увеличивается по мере поступления новых элементов. Результат - 364, сумма всех предоставленных целых чисел:
![](https://habrastorage.org/getpro/habr/upload_files/031/933/271/031933271d94ed864083d0374ed662eb.png)
Мы можем сократить оператор .reduce
следующим образом, что выдаст идентичный результат:
func reduceExample() {
let intPublisher = [3, 9, 27, 81, 244].publisher
intPublisher
.reduce(0, +)
.sink(receiveValue: { print("Sum: \($0)") })
.store(in: &subscriptions)
}
Объединяющие операторы
.prepend
.append
.switchToLatest
.merge(with:)
.combineLatest
.zip
1. prepend
Эта группа операторов позволяет нам отправлять события, значения или другие Publisher
до событий исходного Publisher
:
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! принтятся в последовательном порядке:
![](https://habrastorage.org/getpro/habr/upload_files/9f2/c95/57e/9f2c9557e150ea179cc6beddd043f5a7.png)
Теперь добавим другой Publisher
того же типа:
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
, чтобы оператор .prepend
работал):
![](https://habrastorage.org/getpro/habr/upload_files/d80/dad/247/d80dad247878f3b929efd32962389886.png)
2. append
Оператор .append
работает аналогично .prepend
, но в этом случае мы добавляем значения к исходному Publisher
:
func appendOutputExample() {
let stringPublisher = ["Hello"].publisher
stringPublisher
.append("World!")
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
В результате мы видим как Hello и World! выводятся в консоли:
![](https://habrastorage.org/getpro/habr/upload_files/447/c25/c80/447c25c80d1917f18bbc21afb07b8a3f.png)
Подобно тому, как мы добавляли другой Publisher
раньше, у нас также есть такая же опция с оператором .append
:
![](https://habrastorage.org/getpro/habr/upload_files/7b3/5bc/152/7b35bc15256f088f31a695b185f1bda7.png)
3. switchToLatest
Более сложный оператор .switchToLatest
позволяет нам объединить серию Publisher
в один поток событий:
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
на основнойPassthroughSubject
.stringSubject1
получает значение A.Мы отправляем
stringSubject2
основномуPassthroughSubject
, автоматически игнорируя событияstringSubject1
c этого момента.Точно так же мы отправляем значения в
stringSubject2
. После, подключаемся кstringSubject3
, что заставляет главногоPassthroughSubject
начать игнорировать события отstringSubject2
.
В результате у нас выводятся A, C, D и G:
![](https://habrastorage.org/getpro/habr/upload_files/df1/841/dc6/df1841dc6dea1e9e15688ee5a7d5ecd0.png)
Рассмотрим реальный пример: у нас есть текстовое поле поиска (UITextField
), которое используется для определения доступности какого либо товара в ассортименте. Как только пользователь что-то вводит, мы запускаем запрос.
Проблема заключается в том, что если пользователь введет какое-либо значение, запрос будет осуществлен несмотря на новый ввод в текстовое поле. Наша цель - отменить предыдущий запрос, если пользователь успел ввести новое значение в поле:
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")
}
}
Для простоты примера, функция isAvailable
возвращает случайное значение типа Bool
после некоторой задержки.
Благодаря оператору .switchToLatest
мы добиваемся того, чего хотим. Выводится только одно финальное значение Bool
вместо двух.
![](https://habrastorage.org/getpro/habr/upload_files/c1c/df7/a89/c1cdf7a89251c1f025ebcdc9e85fc35c.png)
4. merge(with:)
Мы используем .merge(with:)
для объединения двух Publisher
, как если бы мы получали значения только от одного:
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")
}
В результате получается чередующаяся последовательность элементов:
![](https://habrastorage.org/getpro/habr/upload_files/c35/9e1/645/c359e164558d846ef69917f8e3732838.png)
5. combineLatest
Оператор .combineLatest
публикует tuple
, содержащий последнее значение каждого Publisher
.
Рассмотрим следующий реальный пример: у нас есть текстовые поля для имени пользователя и пароля, а также кнопка, позволяющая пройти на следующий экран в приложении. Мы хотим держать кнопку отключенной до тех пор, пока имя пользователя не будет содержать не менее пяти символов, а пароль - не менее восьми символов. Этого можно легко добиться с помощью оператора .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 соответственно, условие удовлетворяется и кнопка активируется:
![](https://habrastorage.org/getpro/habr/upload_files/9a2/1ba/b5f/9a21bab5f524c6e7772219ed2695c5bf.png)
6. zip
Оператор .zip
доставляет пару соответствующих значений от каждого Publisher
. Скажем, мы хотим определить, передали ли оба Publisher
одно и то же значение 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) // Not displayed, as its pair is not yet emitted
}
У нас есть следующие соответствующие значения из intSubject1
и intSubject2
:
0 и 4
1 и 1
6 и 7
Последнее значение, 9, не выводится, поскольку intSubject1
еще не опубликовал соответствующее значение:
![](https://habrastorage.org/getpro/habr/upload_files/2bf/ffb/9c5/2bfffb9c5c79d4e0f06c62df1051a67b.png)
Спасибо за чтение! В следующей статье, мы рассмотрим еще два типа операторов, которые предоставляет нам Combine.