Что такое Combine и зачем он нужен?
Combine — это фреймворк Apple для реактивного программирования, представленный в 2019 году вместе с iOS 13. Он предоставляет декларативный Swift API для обработки событий с течением времени. Combine позволяет объединять операторы обработки асинхронных событий в цепочки обработки данных (pipeline).
Основная задача Combine — упростить работу с асинхронными операциями: сетевыми запросами, уведомлениями, KVO (Key-Value Observing), таймерами, делегатами и callback-функциями. Вместо разрозненных механизмов Combine предлагает единый интерфейс на основе Publisher (издатель) и Subscriber (подписчик).
Ключевые концепции:
- Publisher — источник данных, который может отправлять значения и завершаться (успешно или с ошибкой).
- Subscriber — получатель данных, который реагирует на события от Publisher.
- Operator — метод, который преобразует, фильтрует или комбинирует потоки данных.
- Subscription — связь между Publisher и Subscriber, управляющая потоком данных.
Combine тесно интегрирован с SwiftUI и Foundation, что делает его незаменимым для современных iOS-приложений.
Установка
Combine является встроенным фреймворком Apple. Для его использования не требуется устанавливать дополнительные пакеты через CocoaPods, SPM или Carthage. Он доступен начиная с iOS 13, macOS 10.15, tvOS 13 и watchOS 6.
Чтобы начать использовать Combine, достаточно импортировать его в файле Swift:
import Combine
Если вы используете Swift Package Manager для управления зависимостями, Combine уже включен в стандартную библиотеку. Для проектов, поддерживающих более старые версии ОС, можно использовать OpenCombine — open-source реализацию Combine.
Установка OpenCombine через SPM:
// В Package.swift
dependencies: [
.package(url: "https://github.com/CombineOpenSource/Combine.git", from: "0.5.0")
]
Быстрый старт с примером кода
Рассмотрим простейший пример: создание Publisher из массива и подписка на него.
import Combine
// 1. Создаем Publisher из массива
let publisher = [1, 2, 3, 4, 5].publisher
// 2. Создаем Subscriber
let subscriber = Subscribers.Sink<Int, Never>(
receiveCompletion: { completion in
print("Завершено: \(completion)")
},
receiveValue: { value in
print("Получено значение: \(value)")
}
)
// 3. Подписываемся
publisher.subscribe(subscriber)
Вывод в консоль:
Получено значение: 1
Получено значение: 2
Получено значение: 3
Получено значение: 4
Получено значение: 5
Завершено: finished
Более идиоматичный способ с использованием оператора sink:
import Combine
let cancellable = [1, 2, 3, 4, 5].publisher
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
Обратите внимание: sink возвращает AnyCancellable. Его нужно сохранять, иначе подписка будет отменена сразу после выхода из области видимости.
Основные методы/функции
1. sink(receiveCompletion:receiveValue:)
Что делает: Создает подписчика, который получает все значения и завершение от Publisher. Это самый простой способ подписаться на поток.
Параметры:
receiveCompletion— замыкание, вызываемое при завершении (успешном или с ошибкой). ПринимаетSubscribers.Completion<Failure>.receiveValue— замыкание, вызываемое при получении каждого нового значения.
Пример:
let cancellable = Just("Hello, Combine!")
.sink(receiveCompletion: { print("Готово: \($0)") },
receiveValue: { print("Значение: \($0)") })
// Вывод: Значение: Hello, Combine!
// Готово: finished
2. map(_:)
Что делает: Преобразует каждое значение из Publisher с помощью переданной функции. Аналог map из Swift Collections.
Параметры:
transform— замыкание, принимающее значение типа Output и возвращающее новое значение любого типа.
Пример:
let publisher = [1, 2, 3].publisher
let cancellable = publisher
.map { $0 * 2 }
.sink { print($0) }
// Вывод: 2, 4, 6
3. filter(_:)
Что делает: Пропускает только те значения, которые удовлетворяют условию в замыкании.
Параметры:
isIncluded— замыкание, принимающее значение и возвращающее Bool.
Пример:
let publisher = [1, 2, 3, 4, 5].publisher
let cancellable = publisher
.filter { $0 % 2 == 0 }
.sink { print($0) }
// Вывод: 2, 4
4. flatMap(maxPublishers:_:)
Что делает: Преобразует каждое значение в новый Publisher и "уплощает" результат в один поток. Критически важен для работы с асинхронными операциями.
Параметры:
maxPublishers— максимальное количество одновременно активных внутренних Publisher (по умолчанию.unlimited).transform— замыкание, возвращающее новый Publisher.
Пример:
let publisher = ["Hello", "World"].publisher
let cancellable = publisher
.flatMap { value in
value.publisher // Каждая строка становится Publisher символов
}
.sink { print($0) }
// Вывод: H, e, l, l, o, W, o, r, l, d
5. merge(with:)
Что делает: Объединяет два Publisher в один, чередуя их значения в порядке поступления.
Параметры:
other— второй Publisher того же типа Output и Failure.
Пример:
let publisher1 = [1, 2, 3].publisher
let publisher2 = [4, 5, 6].publisher
let cancellable = publisher1
.merge(with: publisher2)
.sink { print($0) }
// Вывод: 1, 2, 3, 4, 5, 6 (порядок может варьироваться)
6. combineLatest(_:)
Что делает: Объединяет последние значения из двух Publisher. Каждый раз, когда любой из Publisher испускает значение, вызывается замыкание с последними значениями обоих.
Параметры:
other— второй Publisher.transform— замыкание, принимающее два значения и возвращающее результат.
Пример:
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()
let cancellable = publisher1
.combineLatest(publisher2) { "\($0) - \($1)" }
.sink { print($0) }
publisher1.send(1)
publisher2.send("A")
publisher1.send(2)
// Вывод: 1 - A, 2 - A
7. zip(_:)
Что делает: Объединяет пары значений из двух Publisher по индексу. Ждет, пока оба Publisher не испустят следующее значение, и только тогда вызывает замыкание.
Параметры:
other— второй Publisher.transform— замыкание, принимающее два значения.
Пример:
let publisher1 = [1, 2, 3].publisher
let publisher2 = ["A", "B", "C"].publisher
let cancellable = publisher1
.zip(publisher2) { "\($0)\($1)" }
.sink { print($0) }
// Вывод: 1A, 2B, 3C
8. debounce(for:scheduler:options:)
Что делает: Подавляет значения, которые поступают слишком часто. Пропускает значение только после того, как с момента последнего поступления прошло указанное время. Идеально для поисковых строк.
Параметры:
dueTime— время ожидания (например,.seconds(0.5)).scheduler— планировщик, на котором выполняется задержка (обычноRunLoop.mainилиDispatchQueue.main).
Пример:
let subject = PassthroughSubject<String, Never>()
let cancellable = subject
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { print("Поиск: \($0)") }
subject.send("S")
subject.send("Sw")
subject.send("Swi")
subject.send("Swif")
subject.send("Swift")
// Через 0.5 сек: Поиск: Swift
9. throttle(for:scheduler:latest:)
Что делает: Ограничивает частоту отправки значений. В отличие от debounce, throttle гарантирует, что значения будут отправляться не чаще одного раза за указанный интервал.
Параметры:
interval— минимальный интервал между отправками.scheduler— планировщик.latest— если true, отправляется последнее значение за интервал; если false — первое.
Пример:
let subject = PassthroughSubject<Int, Never>()
let cancellable = subject
.throttle(for: .seconds(1), scheduler: RunLoop.main, latest: true)
.sink { print($0) }
subject.send(1)
subject.send(2)
subject.send(3)
// Через 1 сек: 3 (latest = true)
10. catch(_:)
Что делает: Обрабатывает ошибки в потоке. Если Publisher завершается с ошибкой, можно заменить его другим Publisher или значением.
Параметры:
handler— замыкание, принимающее ошибку и возвращающее новый Publisher.
Пример:
enum MyError: Error { case networkError }
let publisher = Fail<Int, MyError>(error: .networkError)
let cancellable = publisher
.catch { error in
Just(-1) // Заменяем ошибку на значение -1
}
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// Вывод: -1, finished
Реальный практический пример использования
Создадим простой менеджер для поиска пользователей через сетевой запрос с использованием Combine. Это типичная задача для iOS-приложения.
import Combine
import Foundation
// MARK: - Модель
struct User: Decodable {
let id: Int
let login: String
let avatar_url: String