Combine — ключевые понятия, Publisher и реактивное программирование

Автор: IT Sectr Опубликовано: 2026-05-03 Время чтения: 11 мин

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 — нативный реактивный фреймворк Apple с типами Publisher, Subscriber, Subject и типами ошибок через Failure
  • Publisher испускает события во времени: Output значения, может завершиться успехом или ошибкой Failure
  • Subscriber получает события из Publisher и может запрашивать определённое количество элементов через Demand
  • Операторы map, flatMap, filter, combineLatest, zip, debounce трансформируют потоки без состояния гонок
  • SwiftUI использует Combine через @Published, ObservableObject и @StateObject для реактивного обновления интерфейса

Что такое Combine?

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

Combine базируется на трёх протоколах: Publisher (испускает значения типа Output, может завершиться с ошибкой типа Failure), Subscriber (получает значения, управляет Demand — количеством запрашиваемых элементов), Subscription (представляет связь Publisher-Subscriber с возможностью cancel). Канал передачи данных инициализируется при subscribe и завершается при cancel, completion или error. Demand — уникальная концепция Combine: Subscriber сообщает Publisher, сколько элементов он готов обработать, реализуя backpressure на уровне протокола.

Publisher и Subscriber: архитектура реактивного потока

Publisher — протокол с двумя ассоциированными типами: Output (тип испускаемых значений) и Failure (тип ошибки, реализующий Error). Если поток не может завершиться ошибкой, Failure указывается как Never — это гарантирует Subscriber, что onReceive будет вызываться только с Output. Встроенные Publisher включают Just (одно значение), Sequence (массив), URLSession.DataTaskPublisher (сетевой запрос), NotificationCenter.Publisher и @Published property wrapper.

swift
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 и Demand

Subscription — это протокол, представляющий активную связь между Publisher и Subscriber. Subscriber получает Subscription в методе receive(subscription:) и вызывает request(_:) для указания Demand: .unlimited (все значения), .max(N) (ограниченное количество) или .none (пауза). Demand может динамически меняться — Subscriber может увеличить или уменьшить количество запрашиваемых элементов в процессе получения данных. Это обеспечивает backpressure без буферизации на стороне Publisher.

Subject и CurrentValueSubject

Subject — это тип, объединяющий Publisher и Subscriber. Subject можно использовать как Publisher (на него подписываются) и одновременно как Subscriber (в него отправляют значения). Combine предоставляет два вида Subject: PassthroughSubject (не хранит состояние, передаёт только новые значения) и CurrentValueSubject (хранит текущее значение и передаёт его новым подписчикам). Subject необходим для интеграции императивного кода в реактивные цепочки Combine.

swift
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
Управление Demandbuffer / collectГруппировка или буферизация

flatMap в Combine имеет важное отличие от RxSwift-версии: он принимает closure, возвращающий Publisher с тем же Failure типом, и разворачивает вложенный Publisher в основной поток. flatMap с maxPublishers: .max(1) ведёт себя как switchMap — отменяет предыдущий вложенный Publisher при поступлении нового значения. Это критично для сценариев поиска: при вводе нового символа предыдущий HTTP-запрос автоматически отменяется.

swift
// Дебаунс-поиск с отменой предыдущего запроса
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 — мультикаст потока на множество подписчиков без повторного выполнения.

Schedulers и управление потоками

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.

swift
// Фоновая загрузка + 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.

Combine и SwiftUI: интеграция через ObservableObject

ObservableObject — протокол SwiftUI для объектов, публикующих изменения. Класс, реализующий ObservableObject, может использовать property wrapper @Published для свойств, изменения которых автоматически уведомляют SwiftUI о необходимости перерисовки. Под капотом @Published создаёт Publisher, который уведомляет objectWillChange Publisher при изменении wrappedValue. SwiftUI подписывается на objectWillChange через @StateObject, @ObservedObject или @EnvironmentObject.

@Published и @StateObject

@Published — самый распространённый способ интеграции Combine в SwiftUI. Когда значение @Published-свойства изменяется, SwiftUI обновляет все View, использующие этот объект. @StateObject создаёт экземпляр ObservableObject и подписывается на его изменения. View, созданная с @StateObject, автоматически перерисовывается при изменении @Published-свойств. Если объект должен передаваться между множеством View, используется @ObservedObject или @EnvironmentObject.

swift
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 vs RxSwift

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 остаётся обоснованным решением.

ХарактеристикаCombineRxSwift
РазработчикApple (встроен в SDK)ReactiveX (сообщество)
iOS версияiOS 13+iOS 8+
Тип ошибкиGeneric Failure (Never для UI)Error (любая)
UI интеграция@Published + SwiftUIRxCocoa + UIKit
Операторы~100 встроенных400+ операторов
Swift ConcurrencyЧерез .values (async sequence)Через бридж-библиотеку

Часто задаваемые вопросы

В чём разница между PassthroughSubject и CurrentValueSubject?

PassthroughSubject не хранит состояние — подписчик получает только события, отправленные после подписки. CurrentValueSubject хранит текущее значение и передаёт его каждому новому подписчику сразу при подписке. CurrentValueSubject подходит для представления состояния (например, isLoggedIn).

Как отменить подписку в Combine?

Подписка возвращает AnyCancellable, который отменяется при вызове cancel() или при деинициализации. Для группового управления используйте Set<AnyCancellable> — все подписки отменяются при очистке сета. Это аналог DisposeBag из RxSwift.

Нужно ли изучать Combine после async/await?

Да, Combine остаётся актуальным для потоковых данных: UI-события, debounce, combineLatest, WebSocket. async/await удобен для разовых запросов, Combine — для непрерывных или множественных потоков. Оба фреймворка взаимодополняемы — Publisher можно преобразовать в AsyncSequence.

Как использовать Combine с UIKit?

UIKit не имеет встроенных Publisher, но Apple предоставляет расширения: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. Для пользовательских UI-событий используются PassthroughSubject или @IBAction, обёрнутые в Publisher через Future или Subject.

Что такое backpressure в Combine?

Backpressure — механизм управления скоростью потока: Subscriber сообщает Publisher через Demand, сколько элементов готов обработать. Если Demand = .max(1), Publisher ждёт запроса перед отправкой следующего значения. Это предотвращает переполнение буфера при несовпадении скоростей producer и consumer.

Итоги

  • Combine — нативный реактивный фреймворк Apple для iOS 13+ с паттерном Publisher-Subscriber и строгой типизацией ошибок
  • Publisher и Subscriber образуют канал связи с управлением Demand для backpressure
  • Subject (Passthrough и CurrentValue) интегрируют императивный код в реактивные цепочки
  • Операторы map, flatMap, combineLatest, debounce, catch обеспечивают декларативную обработку потоков
  • Schedulers через DispatchQueue и RunLoop управляют потоками выполнения без блокировки UI
  • SwiftUI использует Combine через @Published, ObservableObject и @StateObject для реактивных View
  • RxSwift остаётся альтернативой для проектов с iOS 8+, Combine — выбор для новых проектов на iOS 13+

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также