Combine — concepte cheie, Publisher și programare reactivă

Autor: IT Sectr Publicat: 2026-05-03 Timp de citire: 11 min

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 — framework-ul reactiv nativ Apple cu tipurile Publisher, Subscriber, Subject și tipuri de erori prin Failure
  • Publisher emite evenimente în timp: valori Output, se poate încheia cu succes sau eroare Failure
  • Subscriber primește evenimente de la Publisher și poate solicita un număr determinat de elemente prin Demand
  • Operatorii map, flatMap, filter, combineLatest, zip, debounce transformă fluxurile fără stări de cursă
  • SwiftUI folosește Combine prin @Published, ObservableObject și @StateObject pentru actualizarea reactivă a interfeței

Ce este Combine?

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.

Conceptele de bază ale Combine

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 și Subscriber: arhitectura fluxului reactiv

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.

swift
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 și Demand

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 și CurrentValueSubject

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.

swift
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:).

Operatori de transformare și combinare

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.

CategorieOperatorScop
Transformaremap / tryMap / flatMapTransformarea valorilor sau fluxurilor
Filtrarefilter / compactMap / removeDuplicatesSelecția sau curățarea valorilor
CombinarecombineLatest / zip / mergeCombinarea mai multor Publisheri
Gestionarea timpuluidebounce / throttle / delayÎntârzierea și subțierea evenimentelor
Gestionarea erorilorcatch / retry / replaceErrorRecuperarea după Failure
Gestionarea Demandbuffer / collectGruparea 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.

swift
// 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ă.

Schedulere și gestionarea firelor de execuție

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.

swift
// Î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.

Combine și SwiftUI: integrare prin ObservableObject

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 și @StateObject

@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.

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

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ăCombineRxSwift
DezvoltatorApple (încorporat în SDK)ReactiveX (comunitate)
Versiune iOSiOS 13+iOS 8+
Tip eroareGeneric Failure (Never pentru UI)Error (oricare)
Integrare UI@Published + SwiftUIRxCocoa + UIKit
Operatori~100 încorporați400+ operatori
Swift ConcurrencyPrin .values (async sequence)Prin bibliotecă punte

Întrebări frecvente

Care este diferența dintre PassthroughSubject și CurrentValueSubject?

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).

Cum se anulează o abonare în Combine?

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.

Este necesar să învăț Combine după async/await?

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.

Cum se utilizează Combine cu UIKit?

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.

Ce este backpressure în Combine?

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

  • Combine — framework-ul reactiv nativ Apple pentru iOS 13+ cu modelul Publisher-Subscriber și tipizare strictă a erorilor
  • Publisher și Subscriber formează un canal de comunicare cu gestionare Demand pentru backpressure
  • Subject (Passthrough și CurrentValue) integrează codul imperativ în lanțurile reactive
  • Operatorii map, flatMap, combineLatest, debounce, catch asigură procesarea declarativă a fluxurilor
  • Schedulerele prin DispatchQueue și RunLoop gestionează firele de execuție fără blocarea UI
  • SwiftUI folosește Combine prin @Published, ObservableObject și @StateObject pentru View-uri reactive
  • RxSwift rămâne alternativă pentru proiecte cu iOS 8+, Combine — alegerea pentru proiecte noi pe iOS 13+

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.

Discutați proiectul

Citiți și