Combine — это фреймворк реактивного программирования от Apple, представленный в iOS 13. По данным Apple Documentation, 2025, Combine предоставляет единый декларативный API для обработки асинхронных событий. Publisher определяет источник данных, а Subscriber подписывается на их получение.
Главное
Combine — это декларативный Swift-фреймворк для обработки асинхронных событий во времени. В отличие от callback-подходов, Combine позволяет описывать пайплайны обработки данных через цепочку операторов. Фреймворк интегрирован с Foundation (URLSession, Timer, NotificationCenter) и SwiftUI (ObservableObject, @Published).
Любой тип, реализующий протокол Publisher, обязан определить Output (тип значений) и Failure (тип ошибки). Publisher не активен до подписки — он начинает эмитить события только после вызова subscribe. Это холодная семантика, характерная для реактивных стримов и повышающая эффективность.
Subscriber получает события через три метода: receive(subscription:) — подтверждение подписки с управлением demand; receive(_:) — получение нового значения; receive(completion:) — завершение потока с успехом или ошибкой. Demand задаёт количество значений, которые Subscriber готов принять — это механизм backpressure.
Apple предоставляет множество встроенных Publisher: Just эмитит одно значение и завершается, Future — асинхронный результат с замыканием, Deferred — отложенное создание Publisher до момента подписки. URLSession.dataTaskPublisher преобразует сетевой запрос в реактивный поток. Timer.publish создаёт периодический таймер. NotificationCenter.default.publisher превращает уведомления в Publisher. Эти интеграции позволяют подписываться на события без ручного моста.
В Combine Publisher и Subscriber связаны протоколом Subscription. Subscription — это связующий объект, который контролирует поток данных. Разработчику не нужно создавать свои типы Publisher — Apple предоставляет встроенные: Just, Future, Deferred, Fail, Empty, а также Publisher для URLSession и Timer.
Subject — это Publisher, в который можно вручную отправлять значения. PassthroughSubject не хранит состояние: подписчик получает только события, которые произошли после подписки. CurrentValueSubject, аналогичный BehaviorRelay в RxSwift, хранит текущее значение и немедленно передаёт его новому подписчику.
Механизм backpressure в Combine регулирует скорость передачи данных. Subscriber сообщает Publisher через subscription.request(.unlimited) или subscription.request(.max(N)), сколько значений он готов обработать. Это предотвращает переполнение буфера при неравномерной скорости producer и consumer.
Combine включает более 100 операторов, разбитых на категории: трансформация (map, tryMap, flatMap), фильтрация (filter, compactMap, removeDuplicates), комбинирование (combineLatest, merge, zip), управление временем (debounce, throttle, delay) и обработка ошибок (catch, replaceError, retry).
Операторы combineLatest объединяет последние значения двух Publisher — каждый раз, когда любой из них эмитит новое значение, замыкание вызывается с обоими обновлёнными значениями. Этот оператор незаменим при валидации форм, где нужно отслеживать состояние нескольких полей ввода одновременно.
Оператор debounce задерживает публикацию значений, пока не пройдёт указанный интервал без новых событий. Это критически важно для поисковых строк: запрос на сервер уходит только после того, как пользователь перестал печатать на 300-500 мс, что снижает нагрузку в 5-10 раз.
Combine предлагает несколько стратегий обработки ошибок на уровне операторов. catch перехватывает Failure и заменяет Publisher на запасной — поток продолжается без краша. replaceError(with:) подставляет значение по умолчанию вместо ошибки. retry(_:) повторяет подписку заданное количество раз при сбое. Важно: ошибка типа Never в сигнатуре Publisher гарантирует, что поток никогда не завершится ошибкой — это позволяет использовать Publisher в SwiftUI без обработки Failure.
Первый пример показывает базовую работу с Just — Publisher, который эмитит одно значение и завершается:
let publisher = Just("Hello Combine")
publisher
.sink(receiveCompletion: { completion in
print("Завершено: \(completion)")
}, receiveValue: { value in
print("Получено: \(value)")
})
Второй пример демонстрирует интеграцию Combine с URLSession для сетевого запроса с обработкой результата на главном потоке:
let url = URL(string: "https://api.example.com")!
URLSession.shared
.dataTaskPublisher(for: url)
.map { $0.data }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Ошибка: \(error)")
}
}, receiveValue: { data in
print("Данных получено: \(data.count)")
})
Третий пример — использование CurrentValueSubject для управления состоянием:
let counter = CurrentValueSubject<Int, Never>(0)
counter
.sink { value in
print("Счётчик: \(value)")
}
counter.send(1) // Выведет: Счётчик: 1
counter.send(2) // Выведет: Счётчик: 2
print(counter.value) // Выведет: 2
Combine и RxSwift решают одну задачу — реактивное программирование, но с разными подходами. Combine — нативный фреймворк Apple, встроенный в систему: не требует установки зависимостей, интегрирован с SwiftUI и Foundation. RxSwift — сторонняя библиотека с поддержкой iOS 9+ и более широким набором операторов.
Производительность Combine выше в типовых сценариях благодаря нативной реализации на C. RxSwift предоставляет больше инструментов для отладки (RxSwift.Resources.total, Debug), но его объектно-ориентированная архитектура может быть менее эффективной на больших потоках данных. Выбор между ними — компромисс между возможностями и интеграцией.
Важное различие — модель backpressure. В Combine Subscriber управляет demand через Subscription, что даёт явный контроль над скоростью потока. В RxSwift backpressure по умолчанию не контролируется — Observable эмитит все значения, и Subscriber обрабатывает их по мере поступления. Для крупных потоков данных Combine оказывается предсказуемее и безопаснее по памяти.
Combine глубоко интегрирован с SwiftUI на уровне языка. @Published генерирует Publisher, ObservableObject предоставляет objectWillChange Publisher. SwiftUI View автоматически подписывается на эти Publisher и перерисовывается при изменениях. Для UIKit есть сторонние обёртки CombineCocoa, но Combine изначально проектировался для SwiftUI, поэтому в UIKit его интеграция требует дополнительного кода. RxSwift, напротив, имеет богатую экосистему для UIKit: RxCocoa предоставляет реактивные обёртки для всех UI-элементов.
Combine поддерживает интеграцию с Core Data через NSFetchedResultsController Publisher. Publisher эмитит новые данные при каждом изменении Core Data стека: добавление, удаление, обновление записей. Это позволяет строить реактивные списки без ручного вызова reloadData. Комбинируя Combine с NotificationCenter Publisher для UIApplication.willResignActiveNotification, разработчик создаёт реактивный слой для всего жизненного цикла приложения без делегатов и колбэков. Для ObservableObject классов SwiftUI автоматически обновляет View при изменении @Published свойств — Combine обеспечивает всю связку без единой строки ручного кода. Модульное тестирование Combine-кода выполняется через XCTestExpectation и Publisher.sink в тестовых кейсах с изоляцией Scheduler через ImmediateScheduler.
При работе с Combine в UIKit проектах разработчики часто используют подписку через sink и хранят AnyCancellable в Set. Типичный паттерн: ViewController создаёт вьюху, хранит cancellables, подписывается на Publisher из ViewModel. При deinit все подписки автоматически отменяются через Cancellables-коллекцию. Для правильной работы с UIControl в Combine можно использовать UIControl.Event Publisher через publisher(for:) расширение.
Отладка Combine осуществляется через оператор print() или handleEvents(). print(String) логирует все события: receive subscription, request demand, receive value, receive completion. handleEvents даёт более тонкий контроль: можно указать замыкания для каждого этапа жизненного цикла Publisher. Для тестирования Combine существует CombineExpectations — библиотека, которая позволяет проверять, что Publisher эмитит ожидаемые значения в тестах с виртуальным временем и контролем demand.
Combine активно используется в SwiftUI для управления формами: каждый @Published property генерирует Publisher, SwiftUI подписывается на него через View.body. Комбинируя несколько Publisher через combineLatest, валидация формы становится декларативной: каждый Publisher отслеживает одно поле, combineLatest собирает все значения, map проверяет их и возвращает статус кнопки Submit. Publishers.Merge объединяет несколько Publisher одного типа в один — полезно для сбора событий с разных контролов в единый поток.
Для интеграции Combine с UIKit используйте UIControl Publisher через расширение: button.publisher(for: .touchUpInside) возвращает Publisher, который эмитит событие при нажатии. CombineCocoa — библиотека от сообщества, предоставляющая Publisher для всех UIControl событий, UITextView.text, UIScrollView.contentOffset. При отписке cancellables Store очищается в deinit, гарантируя, что ни одна подписка не переживёт своего владельца. Для UIControl с множественными состояниями используйте Publishers.MergeMany для объединения Publisher в массив.
Часто задаваемые вопросы
Combine доступен с iOS 13, macOS 10.15, tvOS 13 и watchOS 6. Это ограничивает его использование в проектах, требующих поддержки более старых версий. Для iOS 12 и ниже применяется RxSwift или сторонние библиотеки.
PassthroughSubject не хранит состояние и не повторяет последнее значение новым подписчикам — они получают только будущие события. CurrentValueSubject хранит текущее значение и немедленно передаёт его новому подписчику. CurrentValueSubject.value позволяет читать и писать текущее значение синхронно.
Combine лежит в основе SwiftUI: @Published генерирует Publisher, ObservableObject использует objectWillChange Publisher. SwiftUI View автоматически подписывается на @ObservedObject и @StateObject через Combine-пайплайн, перерисовывая View при изменении свойств.
Backpressure — механизм управления скоростью передачи данных. Subscriber сообщает Subscription, сколько значений готов обработать (demand). Publisher не может эмитить больше, чем запрошено. Это критично при работе с большими потоками данных из сетевых сокетов.
Оператор catch перехватывает ошибку и заменяет Publisher на fallback. replaceError заменяет ошибку на значение по умолчанию. retry повторяет подписку указанное число раз при ошибке. Все операторы следует размещать до subscribe, чтобы исключить краш при Failure.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также