Combine — е native реактивна програмен框架 на Apple, представен в iOS 13, macOS Catalina, tvOS 13 и watchOS 6. Той предоставя декларативен Swift API за обработка на асинхронни събития чрез модела Publisher и Subscriber, заменяйки делегатите, затварянията и NotificationCenter с единна верига. Според Apple, 2025, Combine е основата за SwiftUI и съвременните iOS архитектури, работейки в тясна връзка с async/await и Structured Concurrency. Framework-ът е проектиран за композиция на асинхронни операции с гаранции за безопасност на нишките.
Основни точки
Combine — е декларативен реактивен програмен framework, вграден в Apple SDK. Той имплементира модела Reactive Streams: Publisher произвежда стойности, Subscriber ги консумира, а операторите трансформират потока между тях. Combine решава проблема с callback-ите и делегатите, предоставяйки единен модел за композиция за всякакви асинхронни събития — от мрежови отговори до промени в UI.
Преди появата на Combine, iOS разработчиците използваха библиотеки на трети страни, преди всичко RxSwift. Apple създаде Combine като native алтернатива с дълбока интеграция в екосистемата: framework-ът поддържа 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 остава препоръчителният framework за обработка на поточни данни в 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"
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. Всеки оператор създава нов обект, който се абонира за Publisher-а нагоре по течението и излъчва трансформирани стойности надолу по течението. Тъй като 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 заявка се отменя автоматично.
// Debounce търсене с отмяна на предишна заявка
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 — multicast на потока към множество абонати без повторно изпълнение.
Scheduler в Combine — е протокол, който определя контекста на изпълнение за операторите. За разлика от RxSwift с неговите 5+ вградени Scheduler, Combine използва съществуващите механизми на Apple: DispatchQueue, RunLoop и OperationQueue. Всеки от тези типове съответства на протокола Scheduler, което позволява директното им предаване на receive(on:) и subscribe(on:) без допълнителни адаптери.
receive(on:) превключва надолу по течението към указания Scheduler — еквивалент на observeOn в RxSwift. Всички оператори след receive(on:) се изпълняват на указания Scheduler. subscribe(on:) превключва нагоре по течението — влияе върху изпълнението на 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 — за непрекъснати или множество потоци. И двата framework-а са взаимодопълващи се — 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 изчаква заявка преди да изпрати следващата стойност. Това предотвратява препълване на буфера при несъответствие на скоростите на производителя и потребителя.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също