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.

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.
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 :
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<Int, Never>(0)
// PassthroughSubject : émet des valeurs sans les stocker
// Idéal pour les événements ponctuels (clics, notifications)
let buttonTaps = PassthroughSubject<Void, Never>()
// Future : émet une seule valeur de façon asynchrone
// Encapsule une opération async qui retourne un résultat
let asyncOperation = Future<String, Error> { 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 :
import Combine
// Variable pour stocker les abonnements
// Sans cette référence, l'abonnement serait immédiatement annulé
var cancellables = Set<AnyCancellable>()
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èsToujours 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 :
import Combine
var cancellables = Set<AnyCancellable>()
// 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<User, Never> {
// 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 :
import Combine
var cancellables = Set<AnyCancellable>()
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<String, Never>()
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 300msPrêt à réussir tes entretiens iOS ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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 :
import Combine
var cancellables = Set<AnyCancellable>()
// Simulons un formulaire avec validation
let email = CurrentValueSubject<String, Never>("")
let password = CurrentValueSubject<String, Never>("")
// 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<String, Never>()
let secondAPI = PassthroughSubject<Int, Never>()
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 :
import Combine
var cancellables = Set<AnyCancellable>()
// Plusieurs sources de notifications utilisateur
let pushNotifications = PassthroughSubject<String, Never>()
let localNotifications = PassthroughSubject<String, Never>()
let inAppMessages = PassthroughSubject<String, Never>()
// 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 :
import Combine
var cancellables = Set<AnyCancellable>()
enum APIError: Error {
case networkError
case invalidResponse
case serverError(Int)
}
// Simule un appel API qui peut échouer
func fetchData() -> AnyPublisher<String, APIError> {
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<String> 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)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 :
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<AnyCancellable>()
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 :
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()
}
}Prêt à réussir tes entretiens iOS ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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 :
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 :
import Combine
final class DataManager {
// Set de cancellables : automatiquement annulés à la destruction
private var cancellables = Set<AnyCancellable>()
// 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 :
import Combine
import Foundation
var cancellables = Set<AnyCancellable>()
func loadAndProcessData() -> AnyPublisher<ProcessedData, Error> {
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.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

Combine vs async/await en Swift : patterns de migration progressive
Guide complet sur la migration de Combine vers async/await en Swift : stratégies progressives, bridging patterns, et coexistence des deux paradigmes dans une codebase iOS.

Questions entretien iOS accessibilité en 2026 : VoiceOver et Dynamic Type
Préparez vos entretiens iOS avec les questions clés sur l'accessibilité : VoiceOver, Dynamic Type, traits sémantiques et audits d'accessibilité.

Swift Macros : exemples pratiques de métaprogrammation
Guide complet sur Swift Macros : création de macros freestanding et attached, manipulation de l'AST avec swift-syntax, et exemples pratiques pour réduire le boilerplate.