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-ів та делегатів, надаючи єдину модель композиції для будь-яких асинхронних подій — від мережевих відповідей до змін інтерфейсу користувача.
До появи 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 з можливістю скасування). Канал передачі даних ініціалізується при 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("Завершено")
}
}
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") // Друкує "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 |
| Управління 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 одного типу в один потік, збереження порядку не гарантується. В 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, який сповіщає Publisher objectWillChange при зміні 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.