Combine est le framework natif de programmation réactive d'Apple, présenté dans iOS 13, macOS Catalina, tvOS 13 et watchOS 6. Il fournit une API Swift déclarative pour traiter les événements asynchrones via le modèle Publisher et Subscriber, remplaçant les délégués, les closures et NotificationCenter par une chaîne unifiée. Selon Apple, 2025, Combine est le fondement de SwiftUI et des architectures iOS modernes, travaillant en étroite collaboration avec async/await et Structured Concurrency. Le framework est conçu pour la composition d'opérations asynchrones avec des garanties de sécurité des threads.
Points clés
Combine est un framework de programmation réactive déclarative intégré au SDK d'Apple. Il implémente le modèle Reactive Streams : Publisher produit des valeurs, Subscriber les consomme et les opérateurs transforment le flux entre eux. Combine résout le problème des callbacks et des délégués en fournissant un modèle de composition unifié pour tout événement asynchrone — des réponses réseau aux changements d'interface utilisateur.
Avant Combine, les développeurs iOS utilisaient des bibliothèques tierces, principalement RxSwift. Apple a créé Combine comme une alternative native avec une intégration profonde dans l'écosystème : le framework prend en charge Objective-C via des ponts @objc, fonctionne avec KVO (Key-Value Observing via NSObject.keyValuePublisher) et NotificationCenter, et sert également de base à SwiftUI. Tous les composants UIKit publiés dans SwiftUI utilisent Combine en interne pour les mises à jour des vues.
Combine est conçu en tenant compte de Swift Concurrency : à partir d'iOS 15, un Publisher peut être converti en AsyncSequence via .values et utilisé dans des boucles for-await-in. La conversion inverse de fonctions async en Publisher s'effectue via Future. Selon Apple WWDC 2024, Combine reste le framework recommandé pour le traitement des données en streaming dans les applications UIKit, malgré l'introduction d'async/await pour les appels asynchrones ponctuels.
Combine repose sur trois protocoles : Publisher (émet des valeurs de type Output, peut échouer avec une erreur de type Failure), Subscriber (reçoit les valeurs, gère Demand — le nombre d'éléments demandés), Subscription (représente la connexion Publisher-Subscriber avec possibilité d'annulation). Le canal de données est initialisé lors de l'abonnement et se termine par annulation, achèvement ou erreur. Demand est un concept unique de Combine : le Subscriber indique au Publisher combien d'éléments il est prêt à traiter, implémentant une contre-pression au niveau du protocole.
Publisher est un protocole avec deux types associés : Output (le type des valeurs émises) et Failure (le type d'erreur implémentant Error). Si le flux ne peut pas échouer, Failure est spécifié comme Never — cela garantit au Subscriber que onReceive ne sera appelé qu'avec Output. Les Publishers intégrés incluent Just (valeur unique), Sequence (tableau), URLSession.DataTaskPublisher (requête réseau), NotificationCenter.Publisher et le property wrapper @Published.
import Combine
// Création d'un Publisher à partir d'une séquence
let publisher = [1, 2, 3, 4, 5].publisher
// Création d'un Subscriber avec traitement des valeurs
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("Terminé")
}
}
publisher.subscribe(PrintSubscriber())
Subscription est un protocole représentant la connexion active entre Publisher et Subscriber. Le Subscriber reçoit Subscription dans la méthode receive(subscription:) et appelle request(_:) pour spécifier Demand : .unlimited (toutes les valeurs), .max(N) (quantité limitée) ou .none (pause). Demand peut changer dynamiquement — le Subscriber peut augmenter ou diminuer le nombre d'éléments demandés lors de la réception des données. Cela assure une contre-pression sans mise en mémoire tampon côté Publisher.
Subject est un type qui combine Publisher et Subscriber. Un Subject peut être utilisé comme Publisher (les abonnés s'y abonnent) et simultanément comme Subscriber (des valeurs y sont envoyées). Combine fournit deux types de Subject : PassthroughSubject (ne stocke pas d'état, transmet uniquement les nouvelles valeurs) et CurrentValueSubject (stocke la valeur actuelle et la transmet aux nouveaux abonnés). Subject est nécessaire pour intégrer du code impératif dans les chaînes réactives de Combine.
let subject = PassthroughSubject<String, Never>()
// Abonnement en tant que Publisher
let cancellable = subject
.map { $0.uppercased() }
.sink { print($0) }
// Envoi de valeurs en tant que Subscriber
subject.send("hello") // Affiche "HELLO"
subject.send("world") // Affiche "WORLD"
CurrentValueSubject diffère de PassthroughSubject par la présence d'une valeur initiale et d'une propriété value : un abonné reçoit immédiatement la valeur actuelle lors de l'abonnement, puis toutes les mises à jour ultérieures. CurrentValueSubject.value est lisible et inscriptible — modifier value envoie automatiquement la nouvelle valeur à tous les abonnés. Cela fait de CurrentValueSubject un choix idéal pour représenter l'état dans l'architecture MVVM : ViewModel publie CurrentValueSubject, View s'abonne aux changements via sink.
Les deux Subject peuvent terminer le flux en appelant send(completion: .finished) ou send(completion: .failure(error)). Après la terminaison, le Subject cesse d'accepter et d'émettre des événements. Pour les flux de longue durée qui ne doivent pas se terminer (par exemple, les événements UI), il est recommandé d'utiliser PassthroughSubject avec Never Failure pour éviter d'appeler accidentellement send(completion:).
Les opérateurs Combine sont des méthodes de Publisher qui retournent un nouveau Publisher. Chaque opérateur crée un nouvel objet qui s'abonne au Publisher en amont et émet des valeurs transformées en aval. Comme Publisher est un type générique, les opérateurs maintiennent un typage strict : map transforme Output<A> en Output<B>, tryMap ajoute la possibilité d'erreur. Combine contient environ 100 opérateurs intégrés.
| Catégorie | Opérateur | Objectif |
|---|---|---|
| Transformation | map / tryMap / flatMap | Transformer des valeurs ou des flux |
| Filtrage | filter / compactMap / removeDuplicates | Sélectionner ou nettoyer des valeurs |
| Combinaison | combineLatest / zip / merge | Combiner plusieurs Publishers |
| Contrôle temporel | debounce / throttle / delay | Retarder et espacer les événements |
| Gestion des erreurs | catch / retry / replaceError | Récupération après Failure |
| Gestion de Demand | buffer / collect | Regroupement ou mise en mémoire tampon |
flatMap dans Combine a une différence importante par rapport à la version RxSwift : il accepte une closure qui retourne un Publisher avec le même type Failure et aplatit le Publisher imbriqué dans le flux principal. flatMap avec maxPublishers: .max(1) se comporte comme switchMap — il annule le Publisher imbriqué précédent lorsqu'une nouvelle valeur arrive. Ceci est crucial pour les scénarios de recherche : lorsqu'un nouveau caractère est saisi, la requête HTTP précédente est automatiquement annulée.
// Recherche avec debounce et annulation de la requête précédente
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)
Opérateurs de combinaison — combineLatest et zip — fonctionnent de manière similaire à RxSwift : combineLatest émet un tuple des dernières valeurs de tous les Publishers lorsque l'un d'eux change ; zip associe les valeurs par index. merge combine des Publishers du même type en un seul flux, la préservation de l'ordre n'est pas garantie. Combine dispose également de select — un opérateur rare qui choisit le premier Publisher à se terminer parmi plusieurs, et share — la multidiffusion d'un flux vers plusieurs abonnés sans réexécution.
Scheduler dans Combine est un protocole qui définit le contexte d'exécution des opérateurs. Contrairement à RxSwift avec ses plus de 5 Schedulers intégrés, Combine utilise les mécanismes Apple existants : DispatchQueue, RunLoop et OperationQueue. Chacun de ces types est conforme au protocole Scheduler, permettant de les passer directement à receive(on:) et subscribe(on:) sans adaptateurs supplémentaires.
receive(on:) bascule l'aval vers le Scheduler spécifié — équivalent de observeOn dans RxSwift. Tous les opérateurs après receive(on:) s'exécutent sur le Scheduler spécifié. subscribe(on:) bascule l'amont — affecte l'exécution du Publisher. Un modèle typique : subscribe(on: DispatchQueue.global()) pour le travail en arrière-plan et receive(on: DispatchQueue.main) pour les mises à jour UI. Dans SwiftUI, lors de l'utilisation de .onReceive, la liaison intégrée au thread principal n'est pas nécessaire, mais pour sink, un receive(on:).main explicite est recommandé.
// Chargement en arrière-plan + UI sur le thread 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 est une alternative à DispatchQueue.main pour les opérations UI. La différence est que RunLoop.main est lié au cycle d'événements actuel de l'application, tandis que DispatchQueue.main est lié à la file d'attente globale du thread principal. Pour UIKit, DispatchQueue.main est recommandé ; pour SwiftUI — RunLoop.main. ImmediateWhenScheduler exécute les opérations de manière synchrone sur le thread actuel — utilisé par défaut pour les tests et les Publishers simples.
ObservableObject est un protocole SwiftUI pour les objets qui publient des changements. Une classe implémentant ObservableObject peut utiliser le property wrapper @Published pour des propriétés dont les changements notifient automatiquement SwiftUI de la nécessité de redessiner. Sous le capot, @Published crée un Publisher qui notifie le Publisher objectWillChange lorsque wrappedValue change. SwiftUI s'abonne à objectWillChange via @StateObject, @ObservedObject ou @EnvironmentObject.
@Published est la manière la plus courante d'intégrer Combine dans SwiftUI. Lorsque la valeur d'une propriété @Published change, SwiftUI met à jour toutes les Views utilisant cet objet. @StateObject crée une instance ObservableObject et s'abonne à ses changements. Une View créée avec @StateObject se redessine automatiquement lorsque les propriétés @Published changent. Si l'objet doit être partagé entre plusieurs Views, @ObservedObject ou @EnvironmentObject sont utilisés.
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 est un wrapper d'effacement de type pour Cancellable qui stocke un jeton d'annulation d'abonnement. Set<AnyCancellable> gère le cycle de vie des abonnements : lorsque le propriétaire est désinitialisé, tous les Cancellable sont automatiquement annulés. Dans les projets SwiftUI, Set<AnyCancellable> est déclaré dans la classe ObservableObject, et les abonnements sont ajoutés via .store(in: &cancellables). Pour UIKit, les mêmes mécanismes sont utilisés avec un stockage dans UIViewController via &cancellables ou des appels manuels à cancel().
Combine et RxSwift résolvent les mêmes tâches de programmation réactive mais ont des différences architecturales fondamentales. Combine fait partie du SDK Apple avec une rétrocompatibilité jusqu'à iOS 13, RxSwift est une bibliothèque tierce prenant en charge iOS 8+. Combine utilise un typage strict des erreurs via Failure générique, RxSwift utilise un seul type Error. Combine est intégré à SwiftUI au niveau de la plateforme, RxSwift nécessite RxCocoa pour les extensions UI.
Le choix entre Combine et RxSwift dépend des exigences du projet. Si la version minimale d'iOS est >= 13 et que le projet utilise SwiftUI, Combine est le choix naturel grâce à l'intégration intégrée et à l'absence de dépendances supplémentaires. Si le projet prend en charge iOS 11-12, contient une base de code RxSwift existante ou nécessite des opérateurs spécifiques disponibles uniquement dans RxSwift (par exemple, Observable.from(path:)), RxSwift reste une solution valide.
| Caractéristique | Combine | RxSwift |
|---|---|---|
| Développeur | Apple (intégré au SDK) | ReactiveX (communauté) |
| Version iOS | iOS 13+ | iOS 8+ |
| Type d'erreur | Failure générique (Never pour UI) | Error (quelconque) |
| Intégration UI | @Published + SwiftUI | RxCocoa + UIKit |
| Opérateurs | ~100 intégrés | 400+ opérateurs |
| Swift Concurrency | Via .values (séquence async) | Via bibliothèque pont |
Questions fréquentes
PassthroughSubject ne stocke pas d'état — un abonné reçoit uniquement les événements envoyés après l'abonnement. CurrentValueSubject stocke la valeur actuelle et la transmet à chaque nouvel abonné immédiatement lors de l'abonnement. CurrentValueSubject convient pour représenter un état (par exemple, isLoggedIn).
Un abonnement retourne AnyCancellable, qui est annulé lors de l'appel à cancel() ou à la désinitialisation. Pour la gestion de groupe, utilisez Set<AnyCancellable> — tous les abonnements sont annulés lors du vidage de l'ensemble. C'est l'analogue de DisposeBag dans RxSwift.
Oui, Combine reste pertinent pour les données en streaming : événements UI, debounce, combineLatest, WebSocket. async/await est pratique pour les requêtes ponctuelles, Combine — pour les flux continus ou multiples. Les deux frameworks sont complémentaires — Publisher peut être converti en AsyncSequence.
UIKit n'a pas de Publishers intégrés, mais Apple fournit des extensions : NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. Pour les événements UI personnalisés, PassthroughSubject ou @IBAction enveloppés dans un Publisher via Future ou Subject sont utilisés.
Contre-pression est un mécanisme de contrôle du débit : le Subscriber indique au Publisher via Demand combien d'éléments il est prêt à traiter. Si Demand = .max(1), le Publisher attend une demande avant d'envoyer la valeur suivante. Cela évite le débordement de mémoire tampon lorsque les vitesses du producteur et du consommateur ne correspondent pas.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi