Combine — kluczowe pojęcia, Publisher i programowanie reaktywne

Autor: IT Sectr Opublikowano: 2026-05-03 Czas czytania: 11 min

Combine — to natywny framework programowania reaktywnego od Apple, wprowadzony w iOS 13, macOS Catalina, tvOS 13 i watchOS 6. Oferuje deklaratywny Swift API do przetwarzania zdarzeń asynchronicznych przez wzorzec Publisher i Subscriber, zastępując delegaty, domknięcia i NotificationCenter jednolitą łańcuchem. Według Apple, 2025, Combine jest fundamentem dla SwiftUI i nowoczesnych architektur iOS, działając w ścisłym powiązaniu z async/await i Structured Concurrency. Framework został zaprojektowany do kompozycji operacji asynchronicznych z gwarancją bezpieczeństwa wątkowego.

Najważniejsze

  • Combine — natywny reaktywny framework Apple z typami Publisher, Subscriber, Subject i typami błędów przez Failure
  • Publisher emituje zdarzenia w czasie: wartości Output, może zakończyć się sukcesem lub błędem Failure
  • Subscriber otrzymuje zdarzenia z Publisher i może żądać określonej liczby elementów przez Demand
  • Operatory map, flatMap, filter, combineLatest, zip, debounce przekształcają strumienie bez stanów wyścigu
  • SwiftUI używa Combine przez @Published, ObservableObject i @StateObject do reaktywnego aktualizowania interfejsu

Czym jest Combine?

Combine — to framework deklaratywnego programowania reaktywnego wbudowany w SDK Apple. Implementuje wzorzec Reactive Streams: Publisher produkuje wartości, Subscriber je konsumuje, a operatory przekształcają strumień między nimi. Combine rozwiązuje problem callbacków i delegatów, zapewniając jednolity model kompozycji dla dowolnych zdarzeń asynchronicznych — od odpowiedzi sieciowych po zmiany UI.

Przed pojawieniem się Combine programiści iOS używali bibliotek zewnętrznych, przede wszystkim RxSwift. Apple stworzyła Combine jako natywną alternatywę z głęboką integracją w ekosystem: framework wspiera Objective-C przez mosty @objc, współpracuje z KVO (Key-Value Observing przez NSObject.keyValuePublisher) i NotificationCenter, a także jest podstawą dla SwiftUI. Wszystkie komponenty UIKit opublikowane w SwiftUI używają Combine pod maską do aktualizacji widoków.

Combine został zaprojektowany z myślą o Swift Concurrency: od iOS 15 Publisher można przekształcić w AsyncSequence przez .values i używać w pętlach for-await-in. Odwrotne przekształcenie funkcji async w Publisher wykonuje się przez Future. Według Apple WWDC 2024, Combine pozostaje rekomendowanym frameworkiem do przetwarzania danych strumieniowych w aplikacjach UIKit, pomimo pojawienia się async/await dla pojedynczych wywołań asynchronicznych.

Podstawowe koncepcje Combine

Combine opiera się na trzech protokołach: Publisher (emituje wartości typu Output, może zakończyć się błędem typu Failure), Subscriber (otrzymuje wartości, zarządza Demand — liczbą żądanych elementów), Subscription (reprezentuje połączenie Publisher-Subscriber z możliwością anulowania). Kanał transmisji danych jest inicjowany przy subscribe i kończy się przy cancel, completion lub error. Demand — unikalna koncepcja Combine: Subscriber informuje Publisher, ile elementów jest gotów przetworzyć, realizując backpressure na poziomie protokołu.

Publisher i Subscriber: architektura strumienia reaktywnego

Publisher — protokół z dwoma powiązanymi typami: Output (typ emitowanych wartości) i Failure (typ błędu implementujący Error). Jeśli strumień nie może zakończyć się błędem, Failure jest określany jako Never — gwarantuje to Subscriber, że onReceive będzie wywoływane tylko z Output. Wbudowane Publisherami obejmują Just (jedna wartość), Sequence (tablica), URLSession.DataTaskPublisher (żądanie sieciowe), NotificationCenter.Publisher i @Published property wrapper.

swift
import Combine

// Utwórz Publisher z sekwencji
let publisher = [1, 2, 3, 4, 5].publisher

// Utwórz Subscriber z obsługą wartości
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("Zakończono")
    }
}

publisher.subscribe(PrintSubscriber())

Subscription i Demand

Subscription — to protokół reprezentujący aktywne połączenie między Publisher a Subscriber. Subscriber otrzymuje Subscription w metodzie receive(subscription:) i wywołuje request(_:) w celu określenia Demand: .unlimited (wszystkie wartości), .max(N) (ograniczona liczba) lub .none (pauza). Demand może dynamicznie się zmieniać — Subscriber może zwiększyć lub zmniejszyć liczbę żądanych elementów w trakcie otrzymywania danych. Zapewnia to backpressure bez buforowania po stronie Publisher.

Subject i CurrentValueSubject

Subject — to typ łączący Publisher i Subscriber. Subject może być używany jako Publisher (można się na niego subskrybować) i jednocześnie jako Subscriber (można do niego wysyłać wartości). Combine udostępnia dwa rodzaje Subject: PassthroughSubject (nie przechowuje stanu, przekazuje tylko nowe wartości) i CurrentValueSubject (przechowuje bieżącą wartość i przekazuje ją nowym subskrybentom). Subject jest niezbędny do integracji kodu imperatywnego z reaktywnymi łańcuchami Combine.

swift
let subject = PassthroughSubject<String, Never>()

// Subskrypcja jako Publisher
let cancellable = subject
    .map { $0.uppercased() }
    .sink { print($0) }

// Wysyłanie wartości jako Subscriber
subject.send("hello")  // Wyświetla „HELLO"
subject.send("świat")  // Wyświetla „WORLD"

CurrentValueSubject różni się od PassthroughSubject obecnością wartości początkowej i właściwości value: subskrybent natychmiast otrzymuje bieżącą wartość przy subskrypcji, a następnie wszystkie kolejne aktualizacje. CurrentValueSubject.value jest dostępny do odczytu i zapisu — zmiana value automatycznie wysyła nową wartość do wszystkich subskrybentów. To czyni CurrentValueSubject idealnym wyborem do reprezentacji stanu w architekturze MVVM: ViewModel publikuje CurrentValueSubject, View subskrybuje się ze zmianami przez sink.

Oba Subject pozwalają zakończyć strumień przez wywołanie send(completion: .finished) lub send(completion: .failure(error)). Po zakończeniu Subject przestaje przyjmować i przekazywać zdarzenia. Długotrwałe strumienie, które nie powinny się kończyć (np. zdarzenia UI), zaleca się używać PassthroughSubject z Never Failure, aby wykluczyć przypadkowe wywołanie send(completion:).

Operatory transformacji i łączenia

Operatory Combine — to metody Publisher zwracające nowy Publisher. Każdy operator tworzy nowy obiekt, który subskrybuje się na upstream Publisher i emituje przekształcone wartości downstream. Ponieważ Publisher jest typem generycznym, operatory zachowują ścisłą typizację: map przekształca Output<A> w Output<B>, tryMap dodaje możliwość błędu. Combine zawiera około 100 wbudowanych operatorów.

KategoriaOperatorPrzeznaczenie
Transformacjamap / tryMap / flatMapPrzekształcanie wartości lub strumieni
Filtracjafilter / compactMap / removeDuplicatesSelekcja lub czyszczenie wartości
ŁączeniecombineLatest / zip / mergeŁączenie wielu Publisherów
Zarządzanie czasemdebounce / throttle / delayOpóźnienie i przerzedzanie zdarzeń
Obsługa błędówcatch / retry / replaceErrorOdzyskiwanie po Failure
Zarządzanie Demandbuffer / collectGrupowanie lub buforowanie

flatMap w Combine ma istotną różnicę w porównaniu z wersją RxSwift: przyjmuje closure zwracający Publisher z tym samym typem Failure i rozwija zagnieżdżony Publisher w główny strumień. flatMap z maxPublishers: .max(1) zachowuje się jak switchMap — anuluje poprzedni zagnieżdżony Publisher przy pojawieniu się nowej wartości. Jest to krytyczne dla scenariuszy wyszukiwania: przy wprowadzaniu nowego znaku poprzednie żądanie HTTP jest automatycznie anulowane.

swift
// Wyszukiwanie z debounce i anulowaniem poprzedniego żądania
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)

Operatory łączenia — combineLatest i zip — działają podobnie jak w RxSwift: combineLatest emituje krotkę ostatnich wartości wszystkich Publisherów przy zmianie dowolnego z nich; zip łączy wartości parami według indeksu. merge łączy Publisherami tego samego typu w jeden strumień, preserveOrder nie jest gwarantowane. W Combine dostępny jest również select — rzadki operator wybierający pierwszy zakończony Publisher spośród wielu, oraz share — multicast strumienia do wielu subskrybentów bez ponownego wykonania.

Schedulery i zarządzanie wątkami

Scheduler w Combine — to protokół określający kontekst wykonania dla operatorów. W przeciwieństwie do RxSwift z jego 5+ wbudowanymi Schedulerami, Combine używa istniejących mechanizmów Apple: DispatchQueue, RunLoop i OperationQueue. Każdy z tych typów odpowiada protokołowi Scheduler, co pozwala przekazywać je bezpośrednio do receive(on:) i subscribe(on:) bez dodatkowych adapterów.

receive(on:) przełącza downstream na określony Scheduler — odpowiednik observeOn w RxSwift. Wszystkie operatory po receive(on:) wykonują się na określonym Scheduler. subscribe(on:) przełącza upstream — wpływa na wykonanie Publisher. Typowy wzorzec: subscribe(on: DispatchQueue.global()) do pracy w tle i receive(on: DispatchQueue.main) do aktualizacji UI. W SwiftUI przy użyciu .onReceive wbudowane powiązanie z głównym wątkiem nie jest wymagane, ale dla sink zaleca się jawny receive(on:).main.

swift
// Pobieranie w tle + UI na głównym wątku
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 — alternatywa dla DispatchQueue.main do operacji UI. Różnica polega na tym, że RunLoop.main jest powiązany z bieżącą pętlą zdarzeń aplikacji, podczas gdy DispatchQueue.main — z globalną kolejką głównego wątku. Dla UIKit zaleca się DispatchQueue.main, dla SwiftUI — RunLoop.main. ImmediateWhenScheduler wykonuje operacje synchronicznie na bieżącym wątku — używany domyślnie dla testów i prostych Publisherów.

Combine i SwiftUI: integracja przez ObservableObject

ObservableObject — protokół SwiftUI dla obiektów publikujących zmiany. Klasa implementująca ObservableObject może używać property wrapper @Published dla właściwości, których zmiany automatycznie powiadamiają SwiftUI o konieczności przerysowania. Pod maską @Published tworzy Publisher, który powiadamia Publisher objectWillChange przy zmianie wrappedValue. SwiftUI subskrybuje się na objectWillChange przez @StateObject, @ObservedObject lub @EnvironmentObject.

@Published i @StateObject

@Published — najczęstszy sposób integracji Combine w SwiftUI. Gdy wartość właściwości @Published zmienia się, SwiftUI aktualizuje wszystkie View używające tego obiektu. @StateObject tworzy instancję ObservableObject i subskrybuje się na jej zmiany. View utworzona z @StateObject automatycznie przerysowuje się przy zmianie właściwości @Published. Jeśli obiekt ma być przekazywany między wieloma View, używa się @ObservedObject lub @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 — typ-ścieracz dla Cancellable, przechowujący token anulowania subskrypcji. Set<AnyCancellable> zarządza cyklem życia subskrypcji: przy deinicjalizacji właściciela wszystkie Cancellable są automatycznie anulowane. W projektach SwiftUI Set<AnyCancellable> jest deklarowany w klasie ObservableObject, a subskrypcje dodawane przez .store(in: &cancellables). Dla UIKit stosuje się te same mechanizmy z przechowywaniem w UIViewController przez &cancellables lub ręcznym wywołaniem cancel().

Combine vs RxSwift

Combine i RxSwift rozwiązują te same zadania programowania reaktywnego, ale mają fundamentalne różnice architektoniczne. Combine — część Apple SDK z kompatybilnością wsteczną do iOS 13, RxSwift — biblioteka zewnętrzna z obsługą iOS 8+. Combine używa ścisłej typizacji błędów przez Failure generic, RxSwift — jednolitego typu Error. Combine jest zintegrowany z SwiftUI na poziomie platformy, RxSwift wymaga RxCocoa do rozszerzeń UI.

Wybór między Combine a RxSwift zależy od wymagań projektu. Jeśli minimalna wersja iOS >= 13 i projekt używa SwiftUI — Combine jest naturalnym wyborem dzięki wbudowanej integracji i brakowi dodatkowych zależności. Jeśli projekt obsługuje iOS 11-12, zawiera istniejącą bazę kodu RxSwift lub wymaga specyficznych operatorów dostępnych tylko w RxSwift (np. Observable.from(path:)), — RxSwift pozostaje uzasadnionym rozwiązaniem.

CechaCombineRxSwift
TwórcaApple (wbudowany w SDK)ReactiveX (społeczność)
Wersja iOSiOS 13+iOS 8+
Typ błęduGeneric Failure (Never dla UI)Error (dowolny)
Integracja UI@Published + SwiftUIRxCocoa + UIKit
Operatory~100 wbudowanych400+ operatorów
Swift ConcurrencyPrzez .values (async sequence)Przez bibliotekę mostkującą

Często zadawane pytania

Jaka jest różnica między PassthroughSubject a CurrentValueSubject?

PassthroughSubject nie przechowuje stanu — subskrybent otrzymuje tylko zdarzenia wysłane po subskrypcji. CurrentValueSubject przechowuje bieżącą wartość i przekazuje ją każdemu nowemu subskrybentowi natychmiast przy subskrypcji. CurrentValueSubject nadaje się do reprezentacji stanu (np. isLoggedIn).

Jak anulować subskrypcję w Combine?

Subskrypcja zwraca AnyCancellable, który jest anulowany przy wywołaniu cancel() lub przy deinicjalizacji. Do zarządzania grupowego użyj Set<AnyCancellable> — wszystkie subskrypcje są anulowane przy czyszczeniu setu. To odpowiednik DisposeBag z RxSwift.

Czy trzeba uczyć się Combine po async/await?

Tak, Combine pozostaje aktualny dla danych strumieniowych: zdarzenia UI, debounce, combineLatest, WebSocket. async/await jest wygodny dla pojedynczych żądań, Combine — dla ciągłych lub wielokrotnych strumieni. Oba frameworki są komplementarne — Publisher można przekształcić w AsyncSequence.

Jak używać Combine z UIKit?

UIKit nie ma wbudowanych Publisherów, ale Apple udostępnia rozszerzenia: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. Dla niestandardowych zdarzeń UI używa się PassthroughSubject lub @IBAction owinięte w Publisher przez Future lub Subject.

Czym jest backpressure w Combine?

Backpressure — mechanizm zarządzania prędkością strumienia: Subscriber informuje Publisher przez Demand, ile elementów jest gotów przetworzyć. Jeśli Demand = .max(1), Publisher czeka na żądanie przed wysłaniem kolejnej wartości. Zapobiega to przepełnieniu bufora przy niezgodności prędkości producenta i konsumenta.

Podsumowanie

  • Combine — natywny reaktywny framework Apple dla iOS 13+ ze wzorcem Publisher-Subscriber i ścisłą typizacją błędów
  • Publisher i Subscriber tworzą kanał komunikacji z zarządzaniem Demand dla backpressure
  • Subject (Passthrough i CurrentValue) integrują kod imperatywny z reaktywnymi łańcuchami
  • Operatory map, flatMap, combineLatest, debounce, catch zapewniają deklaratywne przetwarzanie strumieni
  • Schedulery przez DispatchQueue i RunLoop zarządzają wątkami wykonania bez blokowania UI
  • SwiftUI używa Combine przez @Published, ObservableObject i @StateObject do reaktywnych View
  • RxSwift pozostaje alternatywą dla projektów z iOS 8+, Combine — wybór dla nowych projektów na iOS 13+

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również