# Combine Framework : Programmation réactive en Swift > Maîtrisez Combine pour gérer les flux de données asynchrones en Swift : Publishers, Subscribers, Operators et patterns avancés pour applications iOS. - Published: 2026-02-18 - Updated: 2026-03-28 - Author: SharpSkill - Tags: combine, swift, ios, reactive, async - Reading time: 14 min --- La programmation réactive transforme la façon de gérer les événements et données asynchrones dans les applications iOS. Combine, le framework natif d'Apple, offre une approche déclarative et type-safe pour orchestrer ces flux complexes. Ce guide explore les concepts fondamentaux jusqu'aux patterns avancés utilisés en production. > **Pourquoi Combine plutôt que RxSwift ?** > > Combine est intégré nativement à iOS 13+, offre une meilleure performance grâce à son optimisation par Apple, et s'intègre parfaitement avec SwiftUI. Pas de dépendance externe à gérer. ## Les fondements de Combine Combine repose sur trois concepts clés : les **Publishers** qui émettent des valeurs, les **Subscribers** qui les reçoivent, et les **Operators** qui transforment les données entre les deux. Cette architecture permet de créer des pipelines de données réactifs et composables. ### Publisher : la source de données Un Publisher est un type qui peut émettre une séquence de valeurs au fil du temps. Chaque Publisher déclare deux types associés : le type de valeur émise (`Output`) et le type d'erreur possible (`Failure`). Voici comment créer différents types de Publishers : ```swift // PublisherBasics.swift import Combine // Just : émet une seule valeur puis termine // Utile pour convertir une valeur simple en Publisher let singleValue = Just("Bonjour Combine") // CurrentValueSubject : stocke et émet la valeur courante // Parfait pour représenter un état qui change let counter = CurrentValueSubject(0) // PassthroughSubject : émet des valeurs sans les stocker // Idéal pour les événements ponctuels (clics, notifications) let buttonTaps = PassthroughSubject() // Future : émet une seule valeur de façon asynchrone // Encapsule une opération async qui retourne un résultat let asyncOperation = Future { promise in // Simule un appel réseau DispatchQueue.global().asyncAfter(deadline: .now() + 1) { promise(.success("Données chargées")) } } ``` Le type `Never` pour l'erreur signifie que le Publisher ne peut jamais échouer. C'est une garantie au niveau du compilateur qui simplifie le code de gestion d'erreurs. ### Subscriber : recevoir les valeurs Un Subscriber s'abonne à un Publisher pour recevoir ses valeurs. La méthode `sink` est la façon la plus courante de créer un Subscriber. Elle prend deux closures : une pour les erreurs/complétion et une pour chaque valeur reçue : ```swift // SubscriberBasics.swift import Combine // Variable pour stocker les abonnements // Sans cette référence, l'abonnement serait immédiatement annulé var cancellables = Set() let publisher = ["Swift", "Combine", "iOS"].publisher // sink() crée un Subscriber qui reçoit les valeurs publisher .sink( // Appelée quand le Publisher termine ou échoue receiveCompletion: { completion in switch completion { case .finished: print("✅ Terminé avec succès") case .failure(let error): print("❌ Erreur: \(error)") } }, // Appelée pour chaque valeur émise receiveValue: { value in print("Reçu: \(value)") } ) // store() garde une référence à l'abonnement .store(in: &cancellables) // Output: // Reçu: Swift // Reçu: Combine // Reçu: iOS // ✅ Terminé avec succès ``` > **Attention aux fuites mémoire** > > Toujours stocker les `AnyCancellable` retournés par `sink()`. Sans référence, l'abonnement est automatiquement annulé et aucune valeur n'est reçue. ## Transformer les données avec les Operators Les Operators sont le cœur de Combine. Ils permettent de transformer, filtrer et combiner les flux de données de manière déclarative. Chaque Operator retourne un nouveau Publisher, permettant de les chaîner. ### Operators de transformation essentiels Les Operators de transformation modifient chaque valeur émise. `map` transforme les valeurs, `flatMap` aplatit les Publishers imbriqués, et `compactMap` filtre les valeurs nil : ```swift // TransformOperators.swift import Combine var cancellables = Set() // map : transforme chaque valeur // Équivalent du map sur les tableaux [1, 2, 3, 4, 5].publisher .map { $0 * 2 } // Multiplie chaque nombre par 2 .sink { print("Double: \($0)") } .store(in: &cancellables) // Output: 2, 4, 6, 8, 10 // compactMap : transforme ET filtre les nil // Utile pour les conversions optionnelles ["1", "deux", "3", "quatre", "5"].publisher .compactMap { Int($0) } // Convertit en Int, ignore les échecs .sink { print("Nombre valide: \($0)") } .store(in: &cancellables) // Output: 1, 3, 5 // flatMap : aplatit les Publishers imbriqués // Essentiel pour les opérations async en chaîne struct User { let id: Int; let name: String } func fetchUser(id: Int) -> AnyPublisher { // Simule un appel API Just(User(id: id, name: "User \(id)")) .delay(for: .milliseconds(100), scheduler: RunLoop.main) .eraseToAnyPublisher() } [1, 2, 3].publisher .flatMap { id in fetchUser(id: id) } // Chaque ID devient un appel API .sink { user in print("User: \(user.name)") } .store(in: &cancellables) ``` ### Operators de filtrage Les Operators de filtrage contrôlent quelles valeurs passent dans le pipeline. Ils sont essentiels pour éviter les traitements inutiles et optimiser les performances : ```swift // FilterOperators.swift import Combine var cancellables = Set() let numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5].publisher // filter : garde uniquement les valeurs qui satisfont la condition numbers .filter { $0 > 2 } // Garde seulement les nombres > 2 .sink { print("Filtré: \($0)") } .store(in: &cancellables) // Output: 3, 3, 3, 4, 5, 5 // removeDuplicates : supprime les valeurs consécutives identiques numbers .removeDuplicates() // Élimine les doublons consécutifs .sink { print("Sans doublon: \($0)") } .store(in: &cancellables) // Output: 1, 2, 3, 4, 5 // debounce : attend une pause avant d'émettre // Parfait pour la recherche en temps réel let searchText = PassthroughSubject() searchText .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() // Ignore si le texte n'a pas changé .sink { query in print("Recherche: \(query)") // Lancer l'appel API ici } .store(in: &cancellables) // Simule une frappe rapide searchText.send("S") searchText.send("Sw") searchText.send("Swi") searchText.send("Swift") // Seul "Swift" est émis après 300ms ``` ## Combiner plusieurs Publishers Les applications réelles nécessitent souvent de combiner plusieurs sources de données. Combine propose plusieurs Operators pour orchestrer ces flux multiples. ### CombineLatest et Zip `combineLatest` émet dès qu'un des Publishers émet, en combinant avec la dernière valeur des autres. `zip` attend que tous les Publishers aient émis avant de combiner : ```swift // CombiningPublishers.swift import Combine var cancellables = Set() // Simulons un formulaire avec validation let email = CurrentValueSubject("") let password = CurrentValueSubject("") // combineLatest : combine les dernières valeurs de chaque Publisher // Émet à chaque changement de l'un ou l'autre Publishers.CombineLatest(email, password) .map { email, password in // Valide que l'email contient @ et le mot de passe > 6 chars let isEmailValid = email.contains("@") let isPasswordValid = password.count >= 6 return isEmailValid && isPasswordValid } .sink { isFormValid in print("Formulaire valide: \(isFormValid)") } .store(in: &cancellables) email.send("user@example.com") // false (password vide) password.send("123456") // true (les deux sont valides) // zip : attend une valeur de chaque Publisher avant d'émettre // Utile pour synchroniser des opérations parallèles let firstAPI = PassthroughSubject() let secondAPI = PassthroughSubject() Publishers.Zip(firstAPI, secondAPI) .sink { stringValue, intValue in print("Reçu paire: \(stringValue), \(intValue)") } .store(in: &cancellables) firstAPI.send("Hello") // Pas d'émission, attend secondAPI secondAPI.send(42) // Émet: ("Hello", 42) firstAPI.send("World") // Pas d'émission, attend secondAPI secondAPI.send(100) // Émet: ("World", 100) ``` ### Merge pour unifier les flux `merge` combine plusieurs Publishers du même type en un seul flux. Les valeurs arrivent dans l'ordre d'émission, quel que soit le Publisher source : ```swift // MergePublishers.swift import Combine var cancellables = Set() // Plusieurs sources de notifications utilisateur let pushNotifications = PassthroughSubject() let localNotifications = PassthroughSubject() let inAppMessages = PassthroughSubject() // Merge unifie tous les flux en un seul Publishers.Merge3(pushNotifications, localNotifications, inAppMessages) .sink { message in // Traite toutes les notifications de la même façon print("📬 Notification: \(message)") } .store(in: &cancellables) pushNotifications.send("Nouveau message") // 📬 Notification: Nouveau message localNotifications.send("Rappel: réunion") // 📬 Notification: Rappel: réunion inAppMessages.send("Bienvenue !") // 📬 Notification: Bienvenue ! ``` ## Gestion des erreurs dans Combine La gestion des erreurs est intégrée au cœur de Combine. Le type `Failure` des Publishers permet au compilateur de vérifier que toutes les erreurs sont gérées. ### Stratégies de récupération Combine offre plusieurs Operators pour gérer les erreurs : `catch` pour remplacer par un autre Publisher, `retry` pour réessayer, et `replaceError` pour une valeur par défaut : ```swift // ErrorHandling.swift import Combine var cancellables = Set() enum APIError: Error { case networkError case invalidResponse case serverError(Int) } // Simule un appel API qui peut échouer func fetchData() -> AnyPublisher { Fail(error: APIError.networkError) .eraseToAnyPublisher() } // retry : réessaie N fois avant de propager l'erreur fetchData() .retry(3) // Essaie jusqu'à 3 fois .catch { error -> Just in // catch : remplace l'erreur par un Publisher de secours print("Erreur après 3 tentatives: \(error)") return Just("Données en cache") // Valeur de fallback } .sink( receiveCompletion: { _ in }, receiveValue: { print("Résultat: \($0)") } ) .store(in: &cancellables) // replaceError : remplace toute erreur par une valeur fixe // Plus simple que catch quand on veut juste une valeur par défaut fetchData() .replaceError(with: "Erreur - valeur par défaut") .sink { print("Avec fallback: \($0)") } .store(in: &cancellables) ``` > **Never vs Error** > > Utilisez `setFailureType(to:)` pour changer un Publisher `Never` en Publisher qui peut échouer, et `replaceError(with:)` ou `catch` pour faire l'inverse. ## Pattern MVVM avec Combine Combine s'intègre naturellement avec le pattern MVVM (Model-View-ViewModel). Le ViewModel expose des Publishers que la View observe, créant une liaison réactive entre les données et l'interface. ### ViewModel réactif complet Voici un exemple de ViewModel pour une liste d'utilisateurs avec recherche, chargement et gestion d'erreurs : ```swift // UserListViewModel.swift import Combine import Foundation // Modèle de données struct User: Codable, Identifiable { let id: Int let name: String let email: String } // ViewModel avec état réactif final class UserListViewModel: ObservableObject { // MARK: - Published Properties (observés par SwiftUI) @Published var users: [User] = [] // Liste des utilisateurs @Published var searchQuery: String = "" // Texte de recherche @Published var isLoading: Bool = false // État de chargement @Published var errorMessage: String? // Message d'erreur éventuel // MARK: - Private Properties private var cancellables = Set() private let userService: UserServiceProtocol // MARK: - Computed Properties // Filtre les utilisateurs selon la recherche var filteredUsers: [User] { guard !searchQuery.isEmpty else { return users } return users.filter { $0.name.localizedCaseInsensitiveContains(searchQuery) } } // MARK: - Initialization init(userService: UserServiceProtocol = UserService()) { self.userService = userService setupBindings() } // MARK: - Private Methods private func setupBindings() { // Observe les changements de searchQuery // debounce évite les appels trop fréquents $searchQuery .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() .sink { [weak self] query in // Logique de recherche côté serveur si nécessaire print("Recherche mise à jour: \(query)") } .store(in: &cancellables) } // MARK: - Public Methods func loadUsers() { isLoading = true errorMessage = nil userService.fetchUsers() .receive(on: DispatchQueue.main) // Assure l'update UI sur main thread .sink( receiveCompletion: { [weak self] completion in self?.isLoading = false if case .failure(let error) = completion { self?.errorMessage = error.localizedDescription } }, receiveValue: { [weak self] users in self?.users = users } ) .store(in: &cancellables) } } ``` ### Service avec Combine et URLSession URLSession intègre nativement Combine via `dataTaskPublisher`. Voici comment créer un service réseau réutilisable : ```swift // UserService.swift import Combine import Foundation protocol UserServiceProtocol { func fetchUsers() -> AnyPublisher<[User], Error> } final class UserService: UserServiceProtocol { private let baseURL = URL(string: "https://api.example.com")! private let session: URLSession private let decoder: JSONDecoder init(session: URLSession = .shared) { self.session = session self.decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase } func fetchUsers() -> AnyPublisher<[User], Error> { let url = baseURL.appendingPathComponent("users") return session.dataTaskPublisher(for: url) // Vérifie le code de statut HTTP .tryMap { data, response in guard let httpResponse = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } guard 200..<300 ~= httpResponse.statusCode else { throw URLError(.badServerResponse) } return data } // Décode le JSON en modèle Swift .decode(type: [User].self, decoder: decoder) // Efface le type concret pour retourner AnyPublisher .eraseToAnyPublisher() } } ``` ## Intégration avec SwiftUI Combine et SwiftUI forment un duo puissant. Les propriétés `@Published` d'un `ObservableObject` déclenchent automatiquement les mises à jour de la vue. ### Vue SwiftUI avec ViewModel Combine Voici comment connecter le ViewModel à une vue SwiftUI : ```swift // UserListView.swift import SwiftUI struct UserListView: View { // StateObject : crée et possède le ViewModel @StateObject private var viewModel = UserListViewModel() var body: some View { NavigationStack { Group { if viewModel.isLoading { // Indicateur de chargement centré ProgressView("Chargement...") } else if let error = viewModel.errorMessage { // Vue d'erreur avec bouton retry VStack(spacing: 16) { Text("Erreur: \(error)") .foregroundStyle(.red) Button("Réessayer") { viewModel.loadUsers() } } } else { // Liste des utilisateurs List(viewModel.filteredUsers) { user in UserRowView(user: user) } } } .navigationTitle("Utilisateurs") .searchable(text: $viewModel.searchQuery) // Binding direct .onAppear { viewModel.loadUsers() // Charge au premier affichage } } } } struct UserRowView: View { let user: User var body: some View { VStack(alignment: .leading, spacing: 4) { Text(user.name) .font(.headline) Text(user.email) .font(.subheadline) .foregroundStyle(.secondary) } .padding(.vertical, 4) } } ``` ## Patterns avancés ### Annulation et nettoyage automatique La gestion du cycle de vie des abonnements est cruciale pour éviter les fuites mémoire. Le pattern `cancellables` avec `AnyCancellable` assure un nettoyage automatique : ```swift // CancellationPatterns.swift import Combine final class DataManager { // Set de cancellables : automatiquement annulés à la destruction private var cancellables = Set() // Cancellable individuel pour contrôle fin private var currentRequest: AnyCancellable? func startPolling() { // Timer qui émet toutes les 5 secondes Timer.publish(every: 5, on: .main, in: .common) .autoconnect() // Démarre automatiquement .sink { [weak self] _ in self?.fetchLatestData() } .store(in: &cancellables) } func fetchLatestData() { // Annule la requête précédente si elle existe currentRequest?.cancel() currentRequest = URLSession.shared .dataTaskPublisher(for: URL(string: "https://api.example.com/data")!) .map(\.data) .decode(type: [String].self, decoder: JSONDecoder()) .replaceError(with: []) .receive(on: DispatchQueue.main) .sink { data in print("Données reçues: \(data)") } } deinit { // Tous les cancellables sont automatiquement annulés print("DataManager détruit, abonnements annulés") } } ``` ### Scheduler pour le threading Les Schedulers contrôlent sur quel thread les opérations s'exécutent. Utilisez `subscribe(on:)` pour le travail en arrière-plan et `receive(on:)` pour les mises à jour UI : ```swift // SchedulerPatterns.swift import Combine import Foundation var cancellables = Set() func loadAndProcessData() -> AnyPublisher { URLSession.shared.dataTaskPublisher(for: apiURL) // Effectue le parsing sur un thread background .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .map(\.data) .decode(type: RawData.self, decoder: JSONDecoder()) // Traitement lourd sur background thread .map { rawData in // Cette opération coûteuse s'exécute en arrière-plan processData(rawData) } // Retourne sur le main thread pour l'UI .receive(on: DispatchQueue.main) .eraseToAnyPublisher() } ``` ## Conclusion Combine offre une approche puissante et déclarative pour gérer les flux de données asynchrones dans les applications iOS. Les concepts clés à retenir : ✅ **Publishers** émettent des valeurs au fil du temps ✅ **Subscribers** reçoivent et traitent ces valeurs ✅ **Operators** transforment et combinent les flux ✅ **AnyCancellable** gère le cycle de vie des abonnements ✅ **@Published** avec SwiftUI crée des liaisons réactives automatiques ✅ **Schedulers** contrôlent le threading pour des performances optimales La maîtrise de Combine permet de créer des applications iOS robustes, maintenables et réactives. Son intégration native avec SwiftUI en fait un outil incontournable pour le développement iOS moderne. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/ios/combine-programmation-reactive-swift