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 — 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.
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 — 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.
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 — 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 — 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.
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 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.
| Kategoria | Operator | Przeznaczenie |
|---|---|---|
| Transformacja | map / tryMap / flatMap | Przekształcanie wartości lub strumieni |
| Filtracja | filter / compactMap / removeDuplicates | Selekcja lub czyszczenie wartości |
| Łączenie | combineLatest / zip / merge | Łączenie wielu Publisherów |
| Zarządzanie czasem | debounce / throttle / delay | Opóźnienie i przerzedzanie zdarzeń |
| Obsługa błędów | catch / retry / replaceError | Odzyskiwanie po Failure |
| Zarządzanie Demand | buffer / collect | Grupowanie 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.
// 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.
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.
// 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.
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 — 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.
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 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.
| Cecha | Combine | RxSwift |
|---|---|---|
| Twórca | Apple (wbudowany w SDK) | ReactiveX (społeczność) |
| Wersja iOS | iOS 13+ | iOS 8+ |
| Typ błędu | Generic Failure (Never dla UI) | Error (dowolny) |
| Integracja UI | @Published + SwiftUI | RxCocoa + UIKit |
| Operatory | ~100 wbudowanych | 400+ operatorów |
| Swift Concurrency | Przez .values (async sequence) | Przez bibliotekę mostkującą |
Często zadawane pytania
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).
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.
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.
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.
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
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.
Przeczytaj również