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-ів та делегатів, надаючи єдину модель композиції для будь-яких асинхронних подій — від мережевих відповідей до змін інтерфейсу користувача.

До появи 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 з можливістю скасування). Канал передачі даних ініціалізується при 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("Завершено")
    }
}

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")  // Друкує "HELLO"
subject.send("world")  // Друкує "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 одного типу в один потік, збереження порядку не гарантується. В 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, який сповіщає Publisher objectWillChange при зміні 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 (спільнота)
Версія iOSiOS 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

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