Table of Contents
Introduction à la programmation réactive avec Combiner dans iOS
Les applications iOS modernes sont intrinsèquement asynchrones. Des requêtes réseau et requêtes de base de données aux événements d'interface utilisateur et aux données de capteur, les développeurs doivent gérer un flux constant d'informations arrivant à des moments imprévisibles. Traditionnellement, la gestion de ces événements a exigé des délégués, des callbacks ou des centres de notification, qui ont souvent conduit à un code complexe et sensible aux erreurs. Le cadre Combiner, introduit par Apple dans iOS 13, offre une approche unifiée et déclarative de la programmation réactive qui simplifie la gestion des événements asynchrones et des flux de données.
Qu'est-ce que le cadre de combinaison?
Combiner est un cadre Swift natif qui fournit une API déclarative pour le traitement des valeurs dans le temps. Il fait partie de l'écosystème Apples et s'intègre parfaitement à SwiftUI, Fondation et UIKit. Combiner est construit sur trois abstractions principales: les éditeurs émettent des valeurs, les abonnés reçoivent et traitent ces valeurs, et les opérateurs transforment ou filtrent le flux de valeurs. Ensemble, ils créent un pipeline qui définit comment les données circulent dans votre application.
Contrairement aux modèles précédents tels que ou KVO, Combine offre la sécurité du temps de compilation, l'inférence de type et la composition puissante. Il est similaire dans le concept aux extensions réactives comme RxSwift mais est premier parti et profondément intégré avec les plates-formes Apple. Comprendre Combiner est essentiel pour les développeurs iOS modernes, en particulier ceux qui construisent des applications avec SwiftUI, où Combiner puissances beaucoup de fonctionnalités axées sur les données.
Concepts de base de combiner
Avant de plonger dans le code, il est essentiel de saisir les quatre composants primaires qui forment chaque pipeline Combiner:
- Publisher – Type qui peut émettre une séquence de valeurs dans le temps. Il déclare le type de valeurs qu'il affiche () et le type d'erreurs avec lesquelles il peut échouer (. Un éditeur peut émettre un nombre arbitraire de valeurs, puis éventuellement compléter ou échouer.
- Subscriber – Type qui reçoit des valeurs d'un éditeur. Il définit trois méthodes : , (pour gérer une nouvelle valeur), et (pour gérer le succès ou l'échec). La plupart des développeurs utilisent des abonnés prêts à l'emploi comme ou ] au lieu de mettre en œuvre un abonné personnalisé.
- Operator – Un éditeur qui transforme des valeurs d'un éditeur en amont. Des opérateurs comme , , et sont eux-mêmes des éditeurs qui souscrivent à des éditeurs en amont, appliquent une transformation et republissent le résultat.
- Abonnement – La connexion entre un éditeur et un abonné. Un abonnement contrôle le flux de la demande (pression de recul).Les abonnements peuvent être annulés pour arrêter de recevoir des valeurs, qui est géré automatiquement lorsqu'un est dédouané ou annulé manuellement.
De plus, Combine fournit Protocole [et le type de béton ] pour gérer le cycle de vie d'un abonnement.
Création d'éditeurs
Combiner fournit de nombreux éditeurs intégrés qui couvrent des tâches communes asynchrones. Vous pouvez également créer des éditeurs personnalisés en implémentant le protocole ou en utilisant des initialisateurs pratiques.
Editeur de minuterie
Utilisez pour émettre la date courante à intervalles réguliers. Pour démarrer immédiatement le chronomètre, appelez :
let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
L'éditeur émet des valeurs à chaque seconde de la boucle principale.
Éditeur de données de la tâche URLSession
Combiner les extensions avec un éditeur qui effectue des demandes de réseau:
guard let url = URL(string: "https://api.example.com/data") else { return }
let publisher = URLSession.shared.dataTaskPublisher(for: url)
Cet éditeur émet un tuple à la fin, ou un à la fin.
Éditeur de notificationsCenter
Convertissez toute notification en un éditeur :
let notificationPublisher = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
Utile pour réagir aux événements système sans ajouter manuellement des observateurs.
Juste et futur
émet une valeur unique et se termine immédiatement. Il est parfait pour tester ou envelopper une constante dans un éditeur:
let justPublisher = Just("Hello, Combine!")
est un éditeur qui produit éventuellement une valeur unique (ou échoue) asynchrone. Il exécute la fermeture fournie exactement une fois et cache le résultat pour les abonnés ultérieurs:
let futurePublisher = Future<String, Error> { promise in
// Perform async work, then call promise(.success(value)) or promise(.failure(error))
}
TransferthroughSubject et CurrentValueSubject
Les sujets agissent à la fois comme éditeur et abonné, vous permettant d'injecter manuellement des valeurs dans un flux. diffuse des valeurs aux abonnés sans conserver de valeur courante; conserve la dernière valeur et la rejoue aux nouveaux abonnés :
let subject = PassthroughSubject<Int, Never>()
subject.send(1)
subject.send(2)
let currentValueSubject = CurrentValueSubject<String, Never>("initial")
currentValueSubject.send("updated")
print(currentValueSubject.value) // "updated"
S'abonner à un éditeur
Pour activer un pipeline, vous avez besoin d'un abonné. Les plus couramment utilisés sont et .
L'abonné à la rose
fournit une gestion fondée sur la fermeture pour les valeurs reçues et les événements d'achèvement :
let subscription = timerPublisher.sink { time in
print("Timer fired at \(time)")
}
Vous pouvez également gérer les erreurs en fournissant une fermeture :
publisher
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Stream finished")
case .failure(let error):
print("Error: \(error)")
}
}, receiveValue: { value in
print("Received: \(value)")
})
L'abonné à l'attribution
lie la sortie de l'éditeur à une propriété d'un objet, en option par un chemin de touche. La propriété doit être marquée de ou être conforme à KVO:
class ViewModel {
@Published var text: String = ""
}
let viewModel = ViewModel()
let publisher = Just("Hello")
let cancellable = publisher.assign(to: \.text, on: viewModel)
print(viewModel.text) // "Hello"
abonné personnalisé
Pour les cas d'utilisation avancée, vous pouvez implémenter directement le protocole . Cependant, cela est rarement nécessaire; et couvrent la plupart des scénarios.
Transformer les données avec les opérateurs
Les opérateurs sont l'épine dorsale de Combine, vous permettant de composer des pipelines complexes à partir de simples blocs de construction. Voici les plus essentiels:
Carte
Transforme chaque valeur émise en utilisant une fermeture:
publisher
.map { $0 * 2 }
.sink { print($0) }
Filtre
Transmet des valeurs qui satisfont à un prédicat:
publisher
.filter { $0.isMultiple(of: 2) }
.sink { print("Even: \($0)") }
Carte plate
Flattens plusieurs éditeurs internes dans un seul flux. Utilisé couramment lorsque chaque valeur déclenche une nouvelle opération asynchrone:
let searchPublisher = searchTextPublisher
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap { text in
return api.searchPublisher(for: text)
}
Combiner les plus récents
Combine les dernières valeurs de deux éditeurs ou plus en un tuple. Il émet une nouvelle valeur chaque fois que l'un des éditeurs émet:
Publishers.CombineLatest(usernamePublisher, passwordPublisher)
.map { username, password in
return username.count > 0 && password.count > 0
}
.assign(to: \.isEnabled, on: loginButton)
Zip
Paire les valeurs de plusieurs éditeurs dans l'ordre, en attendant que tous les éditeurs émettent la valeur suivante avant d'émettre la paire :
Publishers.Zip(emailsPublisher, namesPublisher)
.sink { email, name in
print("\(name): \(email)")
}
Fusionner
Interfère les valeurs de plusieurs éditeurs de même type en un seul flux :
Publishers.Merge(leftPublisher, rightPublisher)
.sink { print($0) }
Collecte
Les valeurs de tampons dans les tableaux, en émettant une fois que le tampon remplit ou que le flux complète:
publisher
.collect(3) // emit arrays of 3 values
.sink { print($0) }
Déboîte et gaz
attend une pause spécifiée entre les valeurs avant d'émettre la dernière. émet au plus une valeur par intervalle de temps. Les deux sont essentiels pour gérer des événements à haute fréquence comme l'entrée de texte:
textField.textPublisher
.debounce(for: .seconds(0.3), scheduler: RunLoop.main)
.sink { text in
// Perform search after user stops typing
}
Gestion des erreurs dans Combiner
Combiner des pipelines peut échouer. Le type d'éditeur définit les erreurs qu'il peut émettre. Lorsqu'un éditeur échoue, l'abonnement est terminé. Utilisez des opérateurs de gestion des erreurs pour récupérer ou transformer des erreurs.
tryMap et tryFilter
Si votre fermeture de transformation peut lancer, utilisez ou . Le pipeline échoue si une erreur est lancée:
publisher
.tryMap { value in
guard value > 0 else { throw InputError.invalid }
return value * 2
}
Capture
Remplacer un échec par un éditeur alternatif. Utile pour fournir des valeurs de recul:
publisher
.catch { error in
return Just(0) // fallback value
}
Réessayer
Réabonnement automatiquement à l'éditeur en amont un nombre de fois spécifié si elle échoue:
publisher
.retry(3)
.sink(receiveCompletion: { ... }, receiveValue: { ... })
remplacerError et remplacerEmpty
remplace un défaut par une valeur donnée. fournit une valeur si l'éditeur complète sans émettre quoi que ce soit.
Gestion de la mémoire et annulation
Chaque abonnement consomme des ressources et doit être correctement annulé pour éviter les fuites de mémoire. Combiner fournit , une référence en cancellable par type.
Stocker les cancellables dans une collection, généralement un , qui est délocalisé lorsque l'objet propriétaire (p. ex. un contrôleur de vue) est déinitialisé. Par exemple :
private var cancellables = Set<AnyCancellable>()
func setupBindings() {
publisher
.sink { [weak self] value in
self?.updateUI(value)
}
.store(in: &cancellables)
}
Vous pouvez aussi appeler manuellement sur un cancellable, qui met fin à l'abonnement et libère les ressources.
Exemple pratique complet : Demande réseau avec mises à jour de l'interface utilisateur
Let , construit un scénario réel : récupérer les données utilisateur d'une API, décoder et mettre à jour une vue SwiftUI. Cet exemple montre la manipulation des erreurs, les opérateurs et la gestion des threads.
import SwiftUI
import Combine
struct User: Decodable, Identifiable {
let id: Int
let name: String
let email: String
}
class UserViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var errorMessage: String? = nil
private var cancellables = Set<AnyCancellable>()
func fetchUsers() {
isLoading = true
errorMessage = nil
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else {
errorMessage = "Invalid URL"
isLoading = false
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [User].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
self?.isLoading = false
switch completion {
case .finished:
break
case .failure(let error):
self?.errorMessage = error.localizedDescription
}
}, receiveValue: { [weak self] users in
self?.users = users
})
.store(in: &cancellables)
}
}
Points clés:
- extrait les données brutes du tube de réponse.
- utilise Combine , un opérateur intégré pour décoder JSON. Si le décodage échoue, le pipeline se termine par une erreur.
- assure que les mises à jour de l'interface utilisateur se produisent sur le fil principal.
- Entreposer l'abonnement dans relie son cycle de vie au modèle de vision.
- Une erreur est constatée via la propriété , qui peut être liée à un signalement dans SwiftUI.
Combiner et SwiftUI : un partenariat naturel
L'architecture réactive de SwiftUI dépend fortement de Combine. Propriétés annotées avec créer des éditeurs qui avisent automatiquement SwiftUI quand ils changent. Combiner des opérateurs vous permet de transformer des données asynchrones avant qu'elles n'atteignent la vue. Par exemple, vous pouvez débonfler un champ texte , puis récupérer les résultats de recherche et mettre à jour l'interface utilisateur en utilisant Combine .] ou .
L'utilisation de Combine avec SwiftUI élimine la nécessité de modèles KVO manuels ou de délégation, ce qui permet d'obtenir un code plus prévisible et testable.
Combiner les cadres par rapport aux autres cadres réactifs
Avant de combiner, les développeurs iOS utilisaient souvent des cadres tiers comme RxSwift ou ReactiveCocoa[. Combiner offre plusieurs avantages : intégration de la première partie, aucune gestion de dépendance, interopérabilité sans faille avec SwiftUI et la Fondation, et sécurité de type forte. Cependant, RxSwift a un plus grand ensemble d'opérateurs et un écosystème plus mature. Si vous commencez un nouveau projet qui cible iOS 13+, Combiner est le choix recommandé.
Meilleures pratiques pour l'utilisation de combiner
- Soyez simples – Les chaînes complexes peuvent être difficiles à déboguer. Les diviser en éditeurs plus petits, nommés si nécessaire.
- Gérer les abonnements avec précaution – Entreposez toujours les cancellables et évitez de conserver les cycles en utilisant dans les fermetures qui capturent .
- Utiliser des planificateurs appropriés – Utiliser pour passer à la file principale pour les mises à jour de l'interface utilisateur, et pour effectuer des travaux lourds sur les files d'attente de fond.
- Préférence pour les fixations de propriétés simples – Il réduit le code de plaque de chaudière.
- Testez vos pipelines – Combinez la testabilité est excellente parce que vous pouvez créer des éditeurs avec ou et vérifier les sorties en utilisant les attentes.
- Méfiez-vous de la contre-pression – Combiner le système à demande assure aux abonnés de contrôler le flux. Les opérateurs comme ou peuvent aider lorsque les données arrivent plus rapidement qu'elles ne peuvent être traitées.
Conclusion
Le framework Combine révolutionne la façon dont les développeurs iOS gèrent les événements et flux de données asynchrones. En comprenant les éditeurs, les abonnés et les opérateurs, vous pouvez construire des pipelines concis et réactifs qui sont plus faciles à raisonner et à entretenir. Commencez par des exemples simples comme répondre aux notifications ou récupérer des données réseau, puis explorez progressivement des opérateurs avancés comme , et des modèles de récupération d'erreurs. Combiner est une pierre angulaire du développement iOS moderne, surtout lorsqu'il est associé à SwiftUI. La maîtrise vous rendra un développeur plus efficace et vous permettra de créer des applications qui répondent fluidement aux interactions utilisateur et aux sources de données externes.
For further reading, consult the official Apple Combine documentation, Hacking with Swift’s Combine introduction, or Ray Wenderlich’s Combine book for deeper dives.