Combine — este un framework nativ de programare reactivă de la Apple, introdus în iOS 13, macOS Catalina, tvOS 13 și watchOS 6. Oferă o API Swift declarativă pentru procesarea evenimentelor asincrone prin modelul Publisher și Subscriber, înlocuind delegatele, closure-urile și NotificationCenter cu un lanț unificat. Potrivit Apple, 2025, Combine este fundamentul pentru SwiftUI și arhitecturile moderne iOS, lucrând în strânsă legătură cu async/await și Structured Concurrency. Framework-ul este proiectat pentru compoziția operațiilor asincrone cu garanții de siguranță a firelor de execuție.
Principalele puncte
Combine — este un framework de programare reactivă declarativă încorporat în SDK-ul Apple. Acesta implementează modelul Reactive Streams: Publisher produce valori, Subscriber le consumă, iar operatorii transformă fluxul între ele. Combine rezolvă problema callback-urilor și delegatelor, oferind un model unificat de compoziție pentru orice eveniment asincron — de la răspunsuri de rețea la modificări UI.
Înainte de apariția Combine, dezvoltatorii iOS foloseau biblioteci terțe, în primul rând RxSwift. Apple a creat Combine ca o alternativă nativă cu integrare profundă în ecosistem: framework-ul suportă Objective-C prin punți @objc, lucrează cu KVO (Key-Value Observing prin NSObject.keyValuePublisher) și NotificationCenter și este, de asemenea, baza pentru SwiftUI. Toate componentele UIKit publicate în SwiftUI folosesc Combine în culise pentru actualizarea vizualizărilor.
Combine este proiectat ținând cont de Swift Concurrency: începând cu iOS 15, Publisher poate fi transformat în AsyncSequence prin .values și utilizat în bucle for-await-in. Transformarea inversă a funcțiilor async în Publisher se face prin Future. Potrivit Apple WWDC 2024, Combine rămâne framework-ul recomandat pentru procesarea datelor în flux în aplicațiile UIKit, în ciuda apariției async/await pentru apeluri asincrone unice.
Combine se bazează pe trei protocoale: Publisher (emite valori de tip Output, se poate încheia cu o eroare de tip Failure), Subscriber (primește valori, gestionează Demand — numărul de elemente solicitate), Subscription (reprezintă legătura Publisher-Subscriber cu posibilitate de anulare). Canalul de transmitere a datelor se inițializează la subscribe și se încheie la cancel, completion sau error. Demand — concept unic al Combine: Subscriber informează Publisher câte elemente este pregătit să proceseze, implementând backpressure la nivel de protocol.
Publisher — protocol cu două tipuri asociate: Output (tipul valorilor emise) și Failure (tipul erorii care implementează Error). Dacă fluxul nu se poate încheia cu eroare, Failure este specificat ca Never — aceasta garantează Subscriber-ului că onReceive va fi apelat doar cu Output. Publisherii încorporați includ Just (o singură valoare), Sequence (matrice), URLSession.DataTaskPublisher (cerere de rețea), NotificationCenter.Publisher și @Published property wrapper.
import Combine
// Crearea Publisher dintr-o secvență
let publisher = [1, 2, 3, 4, 5].publisher
// Crearea Subscriber cu procesarea valorilor
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("Finalizat")
}
}
publisher.subscribe(PrintSubscriber())
Subscription — este un protocol care reprezintă legătura activă dintre Publisher și Subscriber. Subscriber primește Subscription în metoda receive(subscription:) și apelează request(_:) pentru a specifica Demand: .unlimited (toate valorile), .max(N) (număr limitat) sau .none (pauză). Demand se poate schimba dinamic — Subscriber poate crește sau reduce numărul de elemente solicitate în procesul de primire a datelor. Aceasta asigură backpressure fără tamponare la nivelul Publisher.
Subject — este un tip care combină Publisher și Subscriber. Subject poate fi folosit ca Publisher (se poate abona la el) și simultan ca Subscriber (se pot trimite valori în el). Combine oferă două tipuri de Subject: PassthroughSubject (nu stochează stare, transmite doar valori noi) și CurrentValueSubject (stochează valoarea curentă și o transmite noilor abonați). Subject este necesar pentru integrarea codului imperativ în lanțurile reactive Combine.
let subject = PassthroughSubject<String, Never>()
// Abonare ca Publisher
let cancellable = subject
.map { $0.uppercased() }
.sink { print($0) }
// Trimitere valori ca Subscriber
subject.send("hello") // Afișează „HELLO"
subject.send("lume") // Afișează „WORLD"
CurrentValueSubject se deosebește de PassthroughSubject prin prezența valorii inițiale și a proprietății value: abonatul primește imediat valoarea curentă la abonare, apoi toate actualizările ulterioare. CurrentValueSubject.value este accesibil pentru citire și scriere — modificarea value trimite automat noua valoare tuturor abonaților. Acest lucru face din CurrentValueSubject alegerea ideală pentru reprezentarea stării în arhitectura MVVM: ViewModel publică CurrentValueSubject, View se abonează cu modificări prin sink.
Ambele Subject permit încheierea fluxului prin apelarea send(completion: .finished) sau send(completion: .failure(error)). După încheiere, Subject încetează să primească și să transmită evenimente. Pentru fluxuri de lungă durată care nu ar trebui să se încheie (de exemplu, evenimente UI), se recomandă utilizarea PassthroughSubject cu Never Failure pentru a exclude apelarea accidentală a send(completion:).
Operatorii Combine — sunt metode ale Publisher care returnează un nou Publisher. Fiecare operator creează un nou obiect care se abonează la Publisher-ul din amonte și emite valorile transformate în aval. Deoarece Publisher este un tip generic, operatorii păstrează tipizarea strictă: map transformă Output<A> în Output<B>, tryMap adaugă posibilitatea de eroare. Combine conține aproximativ 100 de operatori încorporați.
| Categorie | Operator | Scop |
|---|---|---|
| Transformare | map / tryMap / flatMap | Transformarea valorilor sau fluxurilor |
| Filtrare | filter / compactMap / removeDuplicates | Selecția sau curățarea valorilor |
| Combinare | combineLatest / zip / merge | Combinarea mai multor Publisheri |
| Gestionarea timpului | debounce / throttle / delay | Întârzierea și subțierea evenimentelor |
| Gestionarea erorilor | catch / retry / replaceError | Recuperarea după Failure |
| Gestionarea Demand | buffer / collect | Gruparea sau tamponarea |
flatMap în Combine are o diferență importantă față de versiunea RxSwift: acceptă un closure care returnează un Publisher cu același tip Failure și desface Publisher-ul imbricat în fluxul principal. flatMap cu maxPublishers: .max(1) se comportă ca switchMap — anulează Publisher-ul imbricat anterior la apariția unei noi valori. Acest lucru este critic pentru scenariile de căutare: la introducerea unui nou caracter, cererea HTTP anterioară este anulată automat.
// Căutare cu debounce și anularea cererii anterioare
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)
Operatorii de combinare — combineLatest și zip — funcționează similar cu RxSwift: combineLatest emite un tuplu al ultimelor valori ale tuturor Publisher-ilor la modificarea oricăruia dintre ei; zip combină valorile în perechi după index. merge combină Publisheri de același tip într-un singur flux, preserveOrder nu este garantat. În Combine există, de asemenea, select — un operator rar care selectează primul Publisher finalizat dintre mai mulți, și share — multicast al fluxului la mai mulți abonați fără execuție repetată.
Scheduler în Combine — este un protocol care definește contextul de execuție pentru operatori. Spre deosebire de RxSwift cu cei 5+ Schedulere încorporate, Combine utilizează mecanismele existente Apple: DispatchQueue, RunLoop și OperationQueue. Fiecare dintre aceste tipuri corespunde protocolului Scheduler, permițând transmiterea lor directă către receive(on:) și subscribe(on:) fără adaptoare suplimentare.
receive(on:) comută avalul pe Scheduler-ul specificat — echivalentul lui observeOn din RxSwift. Toți operatorii după receive(on:) se execută pe Scheduler-ul specificat. subscribe(on:) comută amontele — influențează execuția Publisher-ului. Modelul tipic: subscribe(on: DispatchQueue.global()) pentru lucru în fundal și receive(on: DispatchQueue.main) pentru actualizarea UI. În SwiftUI la utilizarea .onReceive, legătura încorporată la firul principal nu este necesară, dar pentru sink se recomandă receive(on:).main explicit.
// Încărcare în fundal + UI pe firul principal
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 — alternativă la DispatchQueue.main pentru operații UI. Diferența constă în faptul că RunLoop.main este legat de bucla de evenimente curentă a aplicației, în timp ce DispatchQueue.main este legat de coada globală a firului principal. Pentru UIKit se recomandă DispatchQueue.main, pentru SwiftUI — RunLoop.main. ImmediateWhenScheduler execută operațiile sincron pe firul curent — utilizat implicit pentru teste și Publisheri simpli.
ObservableObject — protocol SwiftUI pentru obiecte care publică modificări. Clasa care implementează ObservableObject poate utiliza property wrapper-ul @Published pentru proprietăți ale căror modificări notifică automat SwiftUI despre necesitatea redesării. În culise, @Published creează un Publisher care notifică Publisher-ul objectWillChange la modificarea wrappedValue. SwiftUI se abonează la objectWillChange prin @StateObject, @ObservedObject sau @EnvironmentObject.
@Published — cea mai comună modalitate de integrare a Combine în SwiftUI. Când valoarea unei proprietăți @Published se modifică, SwiftUI actualizează toate View-urile care folosesc acest obiect. @StateObject creează o instanță ObservableObject și se abonează la modificările sale. View-ul creat cu @StateObject se redesenează automat la modificarea proprietăților @Published. Dacă obiectul trebuie transmis între mai multe View-uri, se utilizează @ObservedObject sau @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 — tip-ștergător pentru Cancellable, care stochează token-ul de anulare a abonării. Set<AnyCancellable> gestionează ciclul de viață al abonărilor: la dezinițializarea proprietarului, toate Cancellable-urile sunt anulate automat. În proiectele SwiftUI, Set<AnyCancellable> este declarat în clasa ObservableObject, iar abonările sunt adăugate prin .store(in: &cancellables). Pentru UIKit se folosesc aceleași mecanisme cu stocare în UIViewController prin &cancellables sau apel manual cancel().
Combine și RxSwift rezolvă aceleași sarcini de programare reactivă, dar au diferențe arhitecturale fundamentale. Combine — parte a Apple SDK cu compatibilitate inversă până la iOS 13, RxSwift — bibliotecă terță cu suport pentru iOS 8+. Combine utilizează tipizarea strictă a erorilor prin Failure generic, RxSwift — tipul unificat Error. Combine este integrat cu SwiftUI la nivel de platformă, RxSwift necesită RxCocoa pentru extensii UI.
Alegerea între Combine și RxSwift depinde de cerințele proiectului. Dacă versiunea minimă iOS >= 13 și proiectul utilizează SwiftUI — Combine este alegerea naturală datorită integrării încorporate și absenței dependențelor suplimentare. Dacă proiectul suportă iOS 11-12, conține o bază de cod RxSwift existentă sau necesită operatori specifici disponibili doar în RxSwift (de exemplu, Observable.from(path:)), — RxSwift rămâne o soluție justificată.
| Caracteristică | Combine | RxSwift |
|---|---|---|
| Dezvoltator | Apple (încorporat în SDK) | ReactiveX (comunitate) |
| Versiune iOS | iOS 13+ | iOS 8+ |
| Tip eroare | Generic Failure (Never pentru UI) | Error (oricare) |
| Integrare UI | @Published + SwiftUI | RxCocoa + UIKit |
| Operatori | ~100 încorporați | 400+ operatori |
| Swift Concurrency | Prin .values (async sequence) | Prin bibliotecă punte |
Întrebări frecvente
PassthroughSubject nu stochează stare — abonatul primește doar evenimentele trimise după abonare. CurrentValueSubject stochează valoarea curentă și o transmite fiecărui nou abonat imediat la abonare. CurrentValueSubject este potrivit pentru reprezentarea stării (de exemplu, isLoggedIn).
Abonarea returnează AnyCancellable, care este anulat la apelarea cancel() sau la dezinițializare. Pentru gestionare în grup utilizați Set<AnyCancellable> — toate abonările sunt anulate la curățarea set-ului. Acesta este analogul DisposeBag din RxSwift.
Da, Combine rămâne relevant pentru date în flux: evenimente UI, debounce, combineLatest, WebSocket. async/await este convenabil pentru cereri unice, Combine — pentru fluxuri continue sau multiple. Ambele framework-uri sunt complementare — Publisher poate fi transformat în AsyncSequence.
UIKit nu are Publisheri încorporați, dar Apple oferă extensii: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. Pentru evenimente UI personalizate se utilizează PassthroughSubject sau @IBAction împachetate în Publisher prin Future sau Subject.
Backpressure — mecanism de control al vitezei fluxului: Subscriber informează Publisher prin Demand câte elemente este pregătit să proceseze. Dacă Demand = .max(1), Publisher așteaptă cererea înainte de a trimite următoarea valoare. Aceasta previne supraîncărcarea buffer-ului la nepotrivirea vitezelor producător și consumator.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și