Combine — is een native reactief programmeerframework van Apple, geïntroduceerd in iOS 13, macOS Catalina, tvOS 13 en watchOS 6. Het biedt een declaratieve Swift API voor het verwerken van asynchrone gebeurtenissen via het Publisher- en Subscriber-patroon, waarbij delegates, closures en NotificationCenter worden vervangen door een uniforme keten. Volgens Apple, 2025 is Combine de basis voor SwiftUI en moderne iOS-architecturen, en werkt het nauw samen met async/await en Structured Concurrency. Het framework is ontworpen voor compositie van asynchrone bewerkingen met garanties voor threadveiligheid.
Belangrijkste punten
Combine — is een declaratief reactief programmeerframework ingebouwd in de Apple SDK. Het implementeert het Reactive Streams-patroon: Publisher produceert waarden, Subscriber consumeert ze en operators transformeren de stroom ertussen. Combine lost het probleem van callbacks en delegates op door een uniform compositiemodel te bieden voor elke asynchrone gebeurtenis — van netwerkantwoorden tot UI-wijzigingen.
Vóór de komst van Combine gebruikten iOS-ontwikkelaars bibliotheken van derden, voornamelijk RxSwift. Apple creëerde Combine als een native alternatief met diepe integratie in het ecosysteem: het framework ondersteunt Objective-C via @objc-bruggen, werkt met KVO (Key-Value Observing via NSObject.keyValuePublisher) en NotificationCenter, en is ook de basis voor SwiftUI. Alle UIKit-componenten die in SwiftUI zijn gepubliceerd, gebruiken Combine onder de motorkap om weergaven bij te werken.
Combine is ontworpen met Swift Concurrency in gedachten: vanaf iOS 15 kan Publisher worden omgezet in AsyncSequence via .values en worden gebruikt in for-await-in-lussen. De omgekeerde conversie van async-functies naar Publisher gebeurt via Future. Volgens Apple WWDC 2024 blijft Combine het aanbevolen framework voor het verwerken van streamgegevens in UIKit-applicaties, ondanks de komst van async/await voor eenmalige asynchrone aanroepen.
Combine is gebaseerd op drie protocollen: Publisher (zendt waarden van het type Output uit, kan eindigen met een fout van het type Failure), Subscriber (ontvangt waarden, beheert Demand — het aantal aangevraagde elementen), Subscription (vertegenwoordigt de Publisher-Subscriber-verbinding met annuleringsmogelijkheid). Het gegevenskanaal wordt geïnitialiseerd bij subscribe en eindigt bij cancel, completion of error. Demand — een uniek concept van Combine: Subscriber geeft aan Publisher aan hoeveel elementen het klaar is om te verwerken, en implementeert backpressure op protocolniveau.
Publisher — protocol met twee bijbehorende types: Output (type uitgezonden waarden) en Failure (fouttype dat Error implementeert). Als de stroom niet met een fout kan eindigen, wordt Failure opgegeven als Never — dit garandeert de Subscriber dat onReceive alleen met Output wordt aangeroepen. Ingebouwde Publishers omvatten Just (enkele waarde), Sequence (array), URLSession.DataTaskPublisher (netwerkverzoek), NotificationCenter.Publisher en @Published property wrapper.
import Combine
// Publisher maken van een reeks
let publisher = [1, 2, 3, 4, 5].publisher
// Subscriber maken met waardenverwerking
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("Voltooid")
}
}
publisher.subscribe(PrintSubscriber())
Subscription — is een protocol dat de actieve verbinding tussen Publisher en Subscriber vertegenwoordigt. Subscriber ontvangt Subscription in de methode receive(subscription:) en roept request(_:) aan om Demand op te geven: .unlimited (alle waarden), .max(N) (beperkt aantal) of .none (pauze). Demand kan dynamisch veranderen — Subscriber kan het aantal aangevraagde elementen verhogen of verlagen tijdens het ontvangen van gegevens. Dit zorgt voor backpressure zonder buffering aan de kant van de Publisher.
Subject — is een type dat Publisher en Subscriber combineert. Subject kan worden gebruikt als Publisher (men kan erop abonneren) en tegelijkertijd als Subscriber (men kan er waarden naartoe sturen). Combine biedt twee soorten Subject: PassthroughSubject (slaat geen toestand op, geeft alleen nieuwe waarden door) en CurrentValueSubject (slaat de huidige waarde op en geeft deze door aan nieuwe abonnees). Subject is noodzakelijk voor integratie van imperatieve code in reactieve Combine-ketens.
let subject = PassthroughSubject<String, Never>()
// Abonnement als Publisher
let cancellable = subject
.map { $0.uppercased() }
.sink { print($0) }
// Waarden verzenden als Subscriber
subject.send("hello") // Toont „HELLO"
subject.send("wereld") // Toont „WORLD"
CurrentValueSubject verschilt van PassthroughSubject door de aanwezigheid van een beginwaarde en de eigenschap value: de abonnee ontvangt onmiddellijk de huidige waarde bij het abonneren en vervolgens alle volgende updates. CurrentValueSubject.value is toegankelijk voor lezen en schrijven — het wijzigen van value stuurt automatisch de nieuwe waarde naar alle abonnees. Dit maakt CurrentValueSubject de ideale keuze voor het representeren van toestand in de MVVM-architectuur: ViewModel publiceert CurrentValueSubject, View abonneert zich met wijzigingen via sink.
Beide Subjects maken het mogelijk de stroom te beëindigen door send(completion: .finished) of send(completion: .failure(error)) aan te roepen. Na beëindiging stopt Subject met het ontvangen en doorgeven van gebeurtenissen. Voor langdurige stromen die niet zouden moeten eindigen (bijv. UI-gebeurtenissen), wordt aanbevolen PassthroughSubject met Never Failure te gebruiken om onbedoeld aanroepen van send(completion:) uit te sluiten.
Operators Combine — zijn methoden van Publisher die een nieuwe Publisher retourneren. Elke operator creëert een nieuw object dat zich abonneert op de upstream Publisher en getransformeerde waarden downstream uitgeeft. Omdat Publisher een generiek type is, behouden operators strikte typering: map transformeert Output<A> naar Output<B>, tryMap voegt de mogelijkheid van een fout toe. Combine bevat ongeveer 100 ingebouwde operators.
| Categorie | Operator | Doel |
|---|---|---|
| Transformatie | map / tryMap / flatMap | Transformeren van waarden of stromen |
| Filteren | filter / compactMap / removeDuplicates | Selecteren of opschonen van waarden |
| Combineren | combineLatest / zip / merge | Samenvoegen van meerdere Publishers |
| Tijdbeheer | debounce / throttle / delay | Vertraging en verdunning van gebeurtenissen |
| Foutafhandeling | catch / retry / replaceError | Herstel na Failure |
| Demand-beheer | buffer / collect | Groeperen of bufferen |
flatMap in Combine heeft een belangrijk verschil met de RxSwift-versie: het accepteert een closure die een Publisher retourneert met hetzelfde Failure-type en ontvouwt de geneste Publisher in de hoofdstroom. flatMap met maxPublishers: .max(1) gedraagt zich als switchMap — annuleert de vorige geneste Publisher bij het binnenkomen van een nieuwe waarde. Dit is cruciaal voor zoekscenario's: bij het invoeren van een nieuw teken wordt het vorige HTTP-verzoek automatisch geannuleerd.
// Debounce-zoekopdracht met annulering van vorig verzoek
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)
Combinatieoperators — combineLatest en zip — werken vergelijkbaar met RxSwift: combineLatest zendt een tuple van de laatste waarden van alle Publishers uit bij wijziging van een van hen; zip combineert waarden paarsgewijs op index. merge voegt Publishers van hetzelfde type samen in één stroom, preserveOrder is niet gegarandeerd. In Combine is ook select beschikbaar — een zeldzame operator die de eerste voltooide Publisher uit meerdere selecteert, en share — multicast van de stroom naar meerdere abonnees zonder herhaalde uitvoering.
Scheduler in Combine — is een protocol dat de uitvoeringscontext voor operators definieert. In tegenstelling tot RxSwift met zijn 5+ ingebouwde Schedulers, gebruikt Combine bestaande Apple-mechanismen: DispatchQueue, RunLoop en OperationQueue. Elk van deze types komt overeen met het Scheduler-protocol, waardoor ze direct kunnen worden doorgegeven aan receive(on:) en subscribe(on:) zonder extra adapters.
receive(on:) schakelt downstream over naar de opgegeven Scheduler — equivalent van observeOn in RxSwift. Alle operators na receive(on:) worden uitgevoerd op de opgegeven Scheduler. subscribe(on:) schakelt upstream — beïnvloedt de uitvoering van Publisher. Typisch patroon: subscribe(on: DispatchQueue.global()) voor achtergrondwerk en receive(on: DispatchQueue.main) voor UI-updates. In SwiftUI is bij gebruik van .onReceive ingebouwde binding aan de hoofdthread niet vereist, maar voor sink wordt expliciete receive(on:).main aanbevolen.
// Achtergronddownload + UI op hoofdthread
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 — alternatief voor DispatchQueue.main voor UI-bewerkingen. Het verschil is dat RunLoop.main is gebonden aan de huidige gebeurtenislus van de applicatie, terwijl DispatchQueue.main is gebonden aan de globale wachtrij van de hoofdthread. Voor UIKit wordt DispatchQueue.main aanbevolen, voor SwiftUI — RunLoop.main. ImmediateWhenScheduler voert bewerkingen synchroon uit op de huidige thread — standaard gebruikt voor tests en eenvoudige Publishers.
ObservableObject — SwiftUI-protocol voor objecten die wijzigingen publiceren. Een klasse die ObservableObject implementeert, kan de property wrapper @Published gebruiken voor eigenschappen waarvan wijzigingen automatisch SwiftUI op de hoogte stellen van de noodzaak tot hertekenen. Onder de motorkap creëert @Published een Publisher die de objectWillChange Publisher op de hoogte stelt bij wijziging van wrappedValue. SwiftUI abonneert zich op objectWillChange via @StateObject, @ObservedObject of @EnvironmentObject.
@Published — de meest voorkomende manier om Combine in SwiftUI te integreren. Wanneer de waarde van een @Published-eigenschap verandert, werkt SwiftUI alle Views bij die dit object gebruiken. @StateObject creëert een instantie van ObservableObject en abonneert zich op de wijzigingen ervan. Een View gemaakt met @StateObject wordt automatisch hertekend bij wijziging van @Published-eigenschappen. Als het object tussen meerdere Views moet worden doorgegeven, wordt @ObservedObject of @EnvironmentObject gebruikt.
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 — type-wisser voor Cancellable, die het annuleringstoken van het abonnement opslaat. Set<AnyCancellable> beheert de levenscyclus van abonnementen: bij deïnitialisatie van de eigenaar worden alle Cancellable automatisch geannuleerd. In SwiftUI-projecten wordt Set<AnyCancellable> gedeclareerd in de ObservableObject-klasse en worden abonnementen toegevoegd via .store(in: &cancellables). Voor UIKit worden dezelfde mechanismen gebruikt met opslag in UIViewController via &cancellables of handmatige aanroep van cancel().
Combine en RxSwift lossen dezelfde reactieve programmeertaken op, maar hebben fundamentele architectuurverschillen. Combine — onderdeel van de Apple SDK met achterwaartse compatibiliteit tot iOS 13, RxSwift — bibliotheek van derden met ondersteuning voor iOS 8+. Combine gebruikt strikte fouttypering via Failure generic, RxSwift — het uniforme type Error. Combine is geïntegreerd met SwiftUI op platformniveau, RxSwift vereist RxCocoa voor UI-extensies.
De keuze tussen Combine en RxSwift hangt af van de projectvereisten. Als de minimale iOS-versie >= 13 is en het project SwiftUI gebruikt — is Combine de natuurlijke keuze dankzij ingebouwde integratie en afwezigheid van extra afhankelijkheden. Als het project iOS 11-12 ondersteunt, een bestaande RxSwift-codebasis heeft of specifieke operators vereist die alleen in RxSwift beschikbaar zijn (bijv. Observable.from(path:)), — blijft RxSwift een gerechtvaardigde oplossing.
| Kenmerk | Combine | RxSwift |
|---|---|---|
| Ontwikkelaar | Apple (ingebouwd in SDK) | ReactiveX (community) |
| iOS-versie | iOS 13+ | iOS 8+ |
| Fouttype | Generic Failure (Never voor UI) | Error (willekeurig) |
| UI-integratie | @Published + SwiftUI | RxCocoa + UIKit |
| Operators | ~100 ingebouwd | 400+ operators |
| Swift Concurrency | Via .values (async sequence) | Via brugbibliotheek |
Veelgestelde vragen
PassthroughSubject slaat geen toestand op — de abonnee ontvangt alleen gebeurtenissen die na het abonneren zijn verzonden. CurrentValueSubject slaat de huidige waarde op en geeft deze door aan elke nieuwe abonnee onmiddellijk bij het abonneren. CurrentValueSubject is geschikt voor het weergeven van toestand (bijv. isLoggedIn).
Het abonnement retourneert AnyCancellable, dat wordt geannuleerd bij aanroep van cancel() of bij deïnitialisatie. Gebruik Set<AnyCancellable> voor groepsbeheer — alle abonnementen worden geannuleerd bij het leegmaken van de set. Dit is het equivalent van DisposeBag uit RxSwift.
Ja, Combine blijft relevant voor streamgegevens: UI-gebeurtenissen, debounce, combineLatest, WebSocket. async/await is handig voor eenmalige verzoeken, Combine — voor continue of meervoudige stromen. Beide frameworks zijn complementair — Publisher kan worden omgezet naar AsyncSequence.
UIKit heeft geen ingebouwde Publishers, maar Apple biedt extensies: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. Voor aangepaste UI-gebeurtenissen worden PassthroughSubject of @IBAction gebruikt, verpakt in Publisher via Future of Subject.
Backpressure — een mechanisme voor het regelen van de stroomsnelheid: Subscriber geeft via Demand aan Publisher aan hoeveel elementen het klaar is om te verwerken. Als Demand = .max(1) is, wacht Publisher op een verzoek voordat het de volgende waarde verzendt. Dit voorkomt bufferoverloop bij niet-overeenkomende snelheden van producent en consument.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook