Combine — это нативный фреймворк реактивного программирования от Apple, представленный в iOS 13, macOS Catalina, tvOS 13 и watchOS 6. Он предоставляет декларативный Swift API для обработки асинхронных событий через паттерн Publisher и Subscriber, заменяя делегаты, замыкания и NotificationCenter единообразной цепочкой. По данным Apple, 2025, Combine является фундаментом для SwiftUI и современных iOS-архитектур, работая в тесной связке с async/await и Structured Concurrency. Фреймворк спроектирован для композиции асинхронных операций с гарантиями потокобезопасности.
Главное
Combine — это фреймворк декларативного реактивного программирования, встроенный в SDK Apple. Он реализует паттерн Reactive Streams: Publisher производит значения, Subscriber их потребляет, а операторы трансформируют поток между ними. Combine решает проблему callback-ов и делегатов, предоставляя единую модель композиции для любых асинхронных событий — от сетевых ответов до изменений UI.
До появления Combine iOS-разработчики использовали сторонние библиотеки, прежде всего RxSwift. Apple создала Combine как нативную альтернативу с глубокой интеграцией в экосистему: фреймворк поддерживает Objective-C через @objc-мосты, работает с KVO (Key-Value Observing через NSObject.keyValuePublisher) и NotificationCenter, а также является основой для SwiftUI. Все UIKit-компоненты, опубликованные в SwiftUI, используют Combine под капотом для обновления представлений.
Combine спроектирован с учётом Swift Concurrency: начиная с iOS 15, Publisher можно преобразовать в AsyncSequence через .values и использовать в for-await-in-циклах. Обратное преобразование async-функций в Publisher выполняется через Future. По данным Apple WWDC 2024, Combine остаётся рекомендованным фреймворком для обработки потоковых данных в UIKit-приложениях, несмотря на появление async/await для разовых асинхронных вызовов.
Combine базируется на трёх протоколах: Publisher (испускает значения типа Output, может завершиться с ошибкой типа Failure), Subscriber (получает значения, управляет Demand — количеством запрашиваемых элементов), Subscription (представляет связь Publisher-Subscriber с возможностью cancel). Канал передачи данных инициализируется при subscribe и завершается при cancel, completion или error. Demand — уникальная концепция Combine: Subscriber сообщает Publisher, сколько элементов он готов обработать, реализуя backpressure на уровне протокола.
Publisher — протокол с двумя ассоциированными типами: Output (тип испускаемых значений) и Failure (тип ошибки, реализующий Error). Если поток не может завершиться ошибкой, Failure указывается как Never — это гарантирует Subscriber, что onReceive будет вызываться только с Output. Встроенные Publisher включают Just (одно значение), Sequence (массив), URLSession.DataTaskPublisher (сетевой запрос), NotificationCenter.Publisher и @Published property wrapper.
import Combine
// Создание Publisher из последовательности
let publisher = [1, 2, 3, 4, 5].publisher
// Создание Subscriber с обработкой значений
class PrintSubscriber: Subscriber {
typealias Input = Int
typealias Failure = Never
func receive(subscription: Subscription) {
subscription.request(.unlimited)
}
func receive(_ input: Int) -> Subscribers.Demand {
print("Received: \(input)")
return .unlimited
}
func receive(completion: Subscribers.Completion<Never>) {
print("Completed")
}
}
publisher.subscribe(PrintSubscriber())
Subscription — это протокол, представляющий активную связь между Publisher и Subscriber. Subscriber получает Subscription в методе receive(subscription:) и вызывает request(_:) для указания Demand: .unlimited (все значения), .max(N) (ограниченное количество) или .none (пауза). Demand может динамически меняться — Subscriber может увеличить или уменьшить количество запрашиваемых элементов в процессе получения данных. Это обеспечивает backpressure без буферизации на стороне Publisher.
Subject — это тип, объединяющий Publisher и Subscriber. Subject можно использовать как Publisher (на него подписываются) и одновременно как Subscriber (в него отправляют значения). Combine предоставляет два вида Subject: PassthroughSubject (не хранит состояние, передаёт только новые значения) и CurrentValueSubject (хранит текущее значение и передаёт его новым подписчикам). Subject необходим для интеграции императивного кода в реактивные цепочки Combine.
let subject = PassthroughSubject<String, Never>()
// Подписка как на Publisher
let cancellable = subject
.map { $0.uppercased() }
.sink { print($0) }
// Отправка значений как в Subscriber
subject.send("hello") // Prints "HELLO"
subject.send("world") // Prints "WORLD"
CurrentValueSubject отличается от PassthroughSubject наличием начального значения и свойства value: подписчик сразу получает текущее значение при подписке, а затем все последующие обновления. CurrentValueSubject.value доступен для чтения и записи — изменение value автоматически отправляет новое значение всем подписчикам. Это делает CurrentValueSubject идеальным выбором для представления состояния в архитектуре MVVM: ViewModel публикует CurrentValueSubject, View подписывается с изменениями через sink.
Оба Subject позволяют завершить поток вызовом send(completion: .finished) или send(completion: .failure(error)). После завершения Subject перестаёт принимать и передавать события. Для долгоживущих потоков, которые не должны завершаться (например, UI-события), рекомендуется использовать PassthroughSubject с Never Failure, чтобы исключить случайный вызов send(completion:).
Операторы Combine — это методы Publisher, возвращающие новый Publisher. Каждый оператор создаёт новый объект, который подписывается на upstream Publisher и испускает трансформированные значения downstream. Поскольку Publisher является generic-типом, операторы сохраняют строгую типизацию: map преобразует Output<A> в Output<B>, tryMap добавляет возможность ошибки. Combine содержит около 100 встроенных операторов.
| Категория | Оператор | Назначение |
|---|---|---|
| Трансформация | map / tryMap / flatMap | Преобразование значений или потоков |
| Фильтрация | filter / compactMap / removeDuplicates | Отбор или очистка значений |
| Комбинирование | combineLatest / zip / merge | Объединение нескольких Publisher |
| Управление временем | debounce / throttle / delay | Задержка и прореживание событий |
| Обработка ошибок | catch / retry / replaceError | Восстановление после Failure |
| Управление Demand | buffer / collect | Группировка или буферизация |
flatMap в Combine имеет важное отличие от RxSwift-версии: он принимает closure, возвращающий Publisher с тем же Failure типом, и разворачивает вложенный Publisher в основной поток. flatMap с maxPublishers: .max(1) ведёт себя как switchMap — отменяет предыдущий вложенный Publisher при поступлении нового значения. Это критично для сценариев поиска: при вводе нового символа предыдущий HTTP-запрос автоматически отменяется.
// Дебаунс-поиск с отменой предыдущего запроса
searchTextField.textPublisher
.debounce(for: .seconds(0.3), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap(maxPublishers: .max(1)) { query in
apiService.searchPublisher(query)
.catch { _ in Just([]) }
}
.receive(on: DispatchQueue.main)
.sink { results in
self.tableView.reloadData()
}
.store(in: &cancellables)
Операторы комбинирования — combineLatest и zip — работают аналогично RxSwift: combineLatest испускает кортеж последних значений всех Publisher при изменении любого из них; zip соединяет значения попарно по индексу. merge объединяет Publisher одного типа в один поток, preserveOrder не гарантирован. В Combine также есть select — редкий оператор, выбирающий первый завершившийся Publisher из нескольких, и share — мультикаст потока на множество подписчиков без повторного выполнения.
Scheduler в Combine — это протокол, определяющий контекст выполнения для операторов. В отличие от RxSwift с его 5+ встроенными Scheduler, Combine использует существующие механизмы Apple: DispatchQueue, RunLoop и OperationQueue. Каждый из этих типов соответствует протоколу Scheduler, что позволяет передавать их напрямую в receive(on:) и subscribe(on:) без дополнительных адаптеров.
receive(on:) переключает downstream на указанный Scheduler — эквивалент observeOn в RxSwift. Все операторы после receive(on:) выполняются на указанном Scheduler. subscribe(on:) переключает upstream — влияет на выполнение Publisher. Типичный паттерн: subscribe(on: DispatchQueue.global()) для фоновой работы и receive(on: DispatchQueue.main) для обновления UI. В SwiftUI при использовании .onReceive встроенная привязка к главному потоку не требуется, но для sink рекомендуется явный receive(on:).main.
// Фоновая загрузка + UI на главном потоке
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap { data, response -> Data in
guard let http = response as? HTTPURLResponse,
http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
.receive(on: DispatchQueue.main)
.decode(type: User.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print($0) },
receiveValue: { self.nameLabel.text = $0.name })
.store(in: &cancellables)
RunLoop.main — альтернатива DispatchQueue.main для UI-операций. Разница в том, что RunLoop.main привязан к текущему циклу событий приложения, тогда как DispatchQueue.main — к глобальной очереди главного потока. Для UIKit рекомендуется DispatchQueue.main, для SwiftUI — RunLoop.main. ImmediateWhenScheduler выполняет операции синхронно на текущем потоке — используется по умолчанию для тестов и простых Publisher.
ObservableObject — протокол SwiftUI для объектов, публикующих изменения. Класс, реализующий ObservableObject, может использовать property wrapper @Published для свойств, изменения которых автоматически уведомляют SwiftUI о необходимости перерисовки. Под капотом @Published создаёт Publisher, который уведомляет objectWillChange Publisher при изменении wrappedValue. SwiftUI подписывается на objectWillChange через @StateObject, @ObservedObject или @EnvironmentObject.
@Published — самый распространённый способ интеграции Combine в SwiftUI. Когда значение @Published-свойства изменяется, SwiftUI обновляет все View, использующие этот объект. @StateObject создаёт экземпляр ObservableObject и подписывается на его изменения. View, созданная с @StateObject, автоматически перерисовывается при изменении @Published-свойств. Если объект должен передаваться между множеством View, используется @ObservedObject или @EnvironmentObject.
class UserViewModel: ObservableObject {
@Published var name: String = ""
@Published var age: Int = 0
private var cancellables = Set<AnyCancellable>()
init() {
$name
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { [weak self] newName in
AnalyticsService.logNameChange(newName)
}
.store(in: &cancellables)
}
}
struct UserView: View {
@StateObject var viewModel = UserViewModel()
var body: some View {
TextField("Name", text: $viewModel.name)
}
}
AnyCancellable — тип-стиратель для Cancellable, хранящий токен отмены подписки. Set<AnyCancellable> управляет жизненным циклом подписок: при деинициализации владельца все Cancellable автоматически отменяются. В SwiftUI-проектах Set<AnyCancellable> объявляется в ObservableObject-классе, а подписки добавляются через .store(in: &cancellables). Для UIKit используются те же механизмы с хранением в UIViewController через &cancellables или ручным вызовом cancel().
Combine и RxSwift решают одни и те же задачи реактивного программирования, но имеют фундаментальные архитектурные различия. Combine — часть Apple SDK с обратной совместимостью до iOS 13, RxSwift — сторонняя библиотека с поддержкой iOS 8+. Combine использует строгую типизацию ошибок через Failure generic, RxSwift — единый тип Error. Combine интегрирован с SwiftUI на уровне платформы, RxSwift требует RxCocoa для UI-расширений.
Выбор между Combine и RxSwift зависит от требований проекта. Если минимальная версия iOS >= 13 и проект использует SwiftUI — Combine является естественным выбором благодаря встроенной интеграции и отсутствию дополнительных зависимостей. Если проект поддерживает iOS 11-12, содержит существующую RxSwift-кодовую базу или требует специфических операторов, доступных только в RxSwift (например, Observable.from(path:)), — RxSwift остаётся обоснованным решением.
| Характеристика | Combine | RxSwift |
|---|---|---|
| Разработчик | Apple (встроен в SDK) | ReactiveX (сообщество) |
| iOS версия | iOS 13+ | iOS 8+ |
| Тип ошибки | Generic Failure (Never для UI) | Error (любая) |
| UI интеграция | @Published + SwiftUI | RxCocoa + UIKit |
| Операторы | ~100 встроенных | 400+ операторов |
| Swift Concurrency | Через .values (async sequence) | Через бридж-библиотеку |
Часто задаваемые вопросы
PassthroughSubject не хранит состояние — подписчик получает только события, отправленные после подписки. CurrentValueSubject хранит текущее значение и передаёт его каждому новому подписчику сразу при подписке. CurrentValueSubject подходит для представления состояния (например, isLoggedIn).
Подписка возвращает AnyCancellable, который отменяется при вызове cancel() или при деинициализации. Для группового управления используйте Set<AnyCancellable> — все подписки отменяются при очистке сета. Это аналог DisposeBag из RxSwift.
Да, Combine остаётся актуальным для потоковых данных: UI-события, debounce, combineLatest, WebSocket. async/await удобен для разовых запросов, Combine — для непрерывных или множественных потоков. Оба фреймворка взаимодополняемы — Publisher можно преобразовать в AsyncSequence.
UIKit не имеет встроенных Publisher, но Apple предоставляет расширения: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. Для пользовательских UI-событий используются PassthroughSubject или @IBAction, обёрнутые в Publisher через Future или Subject.
Backpressure — механизм управления скоростью потока: Subscriber сообщает Publisher через Demand, сколько элементов готов обработать. Если Demand = .max(1), Publisher ждёт запроса перед отправкой следующего значения. Это предотвращает переполнение буфера при несовпадении скоростей producer и consumer.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также