Comprendre le Combiner et son rôle dans le développement iOS moderne

Que vous traitiez les réponses réseau, que vous traitiez les entrées des utilisateurs ou que vous coordonniez les mises à jour sur plusieurs composants de l'interface utilisateur, la nécessité d'une approche robuste et déclarative de la programmation asynchrone n'a jamais été plus grande. Le framework Combine d'Apple, introduit avec iOS 13 et pleinement intégré dans l'écosystème Apple, offre un modèle de programmation réactif unifié qui simplifie ces tâches. En modélisant les flux de données en tant qu'éditeurs et abonnés, Combine permet aux développeurs d'écrire un code concis, composable et testable qui répond aux événements au fil du temps.

Cet article vous propose une exploration complète de Combine, de ses abstractions et opérateurs de base aux modèles d'intégration pratiques. Vous apprendrez à configurer les éditeurs et les abonnés, les opérateurs de chaînes pour transformer des données asynchrones, gérer les ressources et la mémoire avec Cancelable, gérer les erreurs gracieusement et intégrer Combine avec SwiftUI et UIKit. D'ici la fin, vous serez équipé pour refactorer des chaînes de rappel complexes en pipelines réactifs plus propres et construire des applications plus résilientes et réactives.

Ce qui est combiné? Un cadre déclaratif pour les événements asynchrones

Combiner est un framework Apple de première partie qui fournit une API Swift déclarative pour le traitement des valeurs au fil du temps. A son cœur, Combiner vous permet de définir un pipeline à travers lequel les événements asynchrones s'écoulent, et d'appliquer des transformations, des filtres ou des effets secondaires à chaque étape. Cette approche contraste avec les modèles traditionnels comme la délégation, les observateurs de centres de notification, ou les gestionnaires de fin d'exécution, qui diffusent souvent la logique à travers plusieurs méthodes et rendent le code plus difficile à raisonner.

Le cadre est construit autour de trois abstractions principales : Éditeurs, qui émettent une séquence de valeurs au fil du temps; Abonnés, qui reçoivent et réagissent à ces valeurs; et Operateurs, qui s'assoient entre éditeurs et abonnés pour transformer, combiner ou serrer le flux. Ensemble, ces composants vous permettent de décrire des flux asynchrones complexes de manière linéaire et compacte.

Apple conçu Combinez pour s'intégrer sans heurt au système type de Swift, assurant la sécurité de la compilation-temps. Cela signifie que de nombreuses erreurs qui pourraient autrement surfacer au moment de l'exécution sont capturées pendant le développement. Combinez également à profit la valeur sémantique de Swift et la gestion de la mémoire, ce qui en fait un ajustement naturel pour les projets iOS modernes.

Pour plus de renseignements, veuillez consulter Apple's Combine documentation[ pour un aperçu des types et des opérateurs disponibles.

Concepts de base : éditeurs, abonnés et opérateurs

Pour utiliser Combinez efficacement, vous avez besoin d'une bonne compréhension de ses trois éléments fondamentaux. Chacun joue un rôle distinct dans le pipeline de données, et la compréhension de leur interaction est essentielle pour concevoir un code réactif efficace.

Éditeurs

Un Publisher est un protocole qui définit un type capable d'émettre une séquence de valeurs dans le temps. Il a deux types associés: , le type de valeurs qu'il envoie, et , le type d'erreur qu'il peut lancer (souvent s'il ne peut pas échouer). Les éditeurs peuvent représenter de nombreuses sources de données asynchrones, y compris des requêtes réseau, des événements chronomètres, des notifications, des observations de valeur clé ou des actions d'interface utilisateur.

Voici quelques éditeurs intégrés communs :

  • – pour les réponses réseau
  • – pour les événements périodiques
  • – pour l'observation des notifications
  • – éditeur à commande manuelle qui diffuse des valeurs à plusieurs abonnés
  • – un sujet qui maintient une valeur courante et publie des mises à jour

Vous pouvez également créer des éditeurs personnalisés en se conformant au protocole , bien que dans la pratique, de nombreux besoins soient satisfaits en combinant les éditeurs existants avec les opérateurs.

Abonnés

Un Subscriber[ est un protocole qui reçoit des valeurs d'un éditeur. Les abonnés définissent comment gérer les données entrantes et les événements de fin de publication (soit la résiliation réussie ou l'échec). Bien que vous puissiez écrire des abonnés personnalisés, la plupart des développeurs utilisent la méthode intégrée , qui accepte les fermetures pour les événements de fin de publication et de valeur, ou , qui lie la sortie d'un éditeur à une propriété sur un objet.

Par exemple:

let publisher = Just("Hello, Combine!")
publisher
 .sink(receiveCompletion: { completion in
 print("Completed: \(completion)")
 }, receiveValue: { value in
 print("Received: \(value)")
 })

Ici crée un abonné implicite qui exécute les fermetures fournies chaque fois qu'une nouvelle valeur arrive ou que le flux se termine.

Opérateurs

Les opérateurs sont des méthodes sur les éditeurs qui retournent un autre éditeur, vous permettant de chaîner les transformations. Combiner fournit un riche ensemble d'opérateurs, y compris:

  • , – valeurs de transformation
  • – ne passez que des valeurs qui satisfont à une condition
  • – transformer et déballer les options
  • , – contrôler le taux de valeurs émises
  • , , – combiner plusieurs flux
  • , – erreurs de gestion
  • – éditeurs nichés aplatis

Les opérateurs forment l'épine dorsale des pipelines réactifs. En les enchaînement, vous pouvez construire des flux de données sophistiqués qui sont faciles à lire et à entretenir.

Configurer Combiner dans votre projet

Combiner fait partie du cadre de la Fondation et est disponible sur iOS 13.0+, macOS 10.15+, watchOS 6.0+ et tvOS 13.0+. Aucune importation supplémentaire n'est nécessaire au-delà de . Cependant, comme Combiner compte beaucoup sur les génériques et les fermetures, vous stockerez généralement des abonnements dans un pour gérer leur cycle de vie.

Toute Cancellable est un objet cancellable à effacement de type qui annule automatiquement l'abonnement lorsqu'il est dédouané. En conservant ces jetons dans une collection (souvent un ensemble sur votre contrôleur de vue ou modèle de vue), vous vous assurez que les abonnements sont nettoyés lorsque l'objet propriétaire est libéré.

var cancellables = Set<AnyCancellable>()

func setupObservation() {
 NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
 .sink { _ in
 print("App became active")
 }
 .store(in: &cancellables)
}

Exemples pratiques utilisant le mélange

Pour comprendre la puissance de Combine, il aide à voir qu'il s'applique aux scénarios du monde réel. Ci-dessous, plusieurs exemples qui démontrent des modèles communs, de la mise à jour de réseau et d'interface utilisateur à la combinaison de plusieurs sources asynchrones.

1. Récupération de données réseau avec manipulation d'erreurs

L'une des utilisations les plus courantes pour Combine est les requêtes réseau. retourne un éditeur qui émet un tuple de . Vous pouvez ensuite utiliser des opérateurs pour extraire et décoder les données.

import Combine
import Foundation

var cancellables = Set<AnyCancellable>()

struct User: Codable {
 let id: Int
 let name: String
}

func fetchUser(id: Int) {
 let url = URL(string: "https://jsonplaceholder.typicode.com/users/\(id)")!
 URLSession.shared.dataTaskPublisher(for: url)
 .map { $0.data }
 .decode(type: User.self, decoder: JSONDecoder())
 .receive(on: DispatchQueue.main)
 .sink(receiveCompletion: { completion in
 switch completion {
 case .finished:
 print("Request completed successfully")
 case .failure(let error):
 print("Request failed with error: \(error.localizedDescription)")
 }
 }, receiveValue: { user in
 print("Fetched user: \(user.name)")
 })
 .store(in: &cancellables)
}

Dans ce pipeline, l'opérateur tente d'analyser les données brutes dans un objet . Si le décodage échoue, l'erreur est relayée au gestionnaire d'achèvement de l'abonné. L'opérateur assure que la fermeture s'exécute sur la file d'attente principale, qui est essentielle pour les mises à jour de l'interface utilisateur.

2. Traitement des entrées utilisateur avec débonflement

Combiner excelle lors du traitement des événements de l'interface utilisateur, comme l'entrée de champ texte. En utilisant , vous pouvez attendre que l'utilisateur arrête de taper avant de déclencher une recherche ou une validation.

class SearchViewModel: ObservableObject {
 @Published var query: String = ""
 @Published var results: [String] = []
 private var cancellables = Set<AnyCancellable>()

 init() {
 $query
 .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
 .removeDuplicates()
 .sink { [weak self] searchText in
 self?.performSearch(with: searchText)
 }
 .store(in: &cancellables)
 }

 private func performSearch(with text: String) {
 // Simulate network search
 print("Searching for: \(text)")
 }
}

L'enveloppeur de propriétés crée un éditeur qui émet de nouvelles valeurs à chaque fois que change. L'opérateur attend 300 millisecondes après le dernier événement, et empêche les recherches en double pour le même texte.

3. Fusionner plusieurs sources asynchrones

Souvent, une application doit combiner des données de plusieurs flux indépendants. Par exemple, vous pourriez devoir attendre qu'un profil utilisateur et une liste d'amis soient chargés avant de mettre à jour l'interface utilisateur. L'opérateur vous permet de fusionner les dernières valeurs de deux éditeurs.

let profilePublisher = URLSession.shared.dataTaskPublisher(for: profileURL)
 .map { $0.data }
 .decode(type: UserProfile.self, decoder: JSONDecoder())

let friendsPublisher = URLSession.shared.dataTaskPublisher(for: friendsURL)
 .map { $0.data }
 .decode(type: [Friend].self, decoder: JSONDecoder())

profilePublisher
 .combineLatest(friendsPublisher)
 .receive(on: DispatchQueue.main)
 .sink(receiveCompletion: { completion in
 // handle errors
 }, receiveValue: { profile, friends in
 // update UI with both profile and friends
 print("Profile: \(profile.name), Friends: \(friends.count)")
 })
 .store(in: &cancellables)

Avec , l'abonné reçoit un tuple contenant les dernières valeurs des deux éditeurs chaque fois qu'ils émettent une nouvelle valeur. Pour les scénarios où vous devez coupler des valeurs dans l'ordre (première, deuxième, deuxième), utilisez plutôt.

4. Récupération d'erreur avec les captures et réessayer

Les réseaux sont peu fiables et Combine fournit aux opérateurs de gérer les échecs gracieusement. L'opérateur vous permet de remplacer un éditeur de retour lorsqu'une erreur se produit, tandis que tente de réabonner un nombre de fois spécifié.

URLSession.shared.dataTaskPublisher(for: url)
 .map { $0.data }
 .decode(type: [String].self, decoder: JSONDecoder())
 .retry(3)
 .catch { error in
 // Return a fallback empty array
 Just([]).setFailureType(to: Error.self)
 }
 .sink(receiveCompletion: { completion in
 // Will not receive failure because catch handled it
 }, receiveValue: { data in
 print("Received \(data.count) items")
 })
 .store(in: &cancellables)

En saisissant l'erreur et en fournissant une valeur par défaut, le flux complète avec succès du point de vue de l'abonné. Ce modèle est particulièrement utile pour fournir une expérience dégradée gracieuse lorsque les données sont indisponibles.

5. Reliure à l'assurance-chômage avec l'attribution

Combine l'opérateur vous permet de lier la sortie d'un éditeur directement à une propriété d'un objet, éliminant ainsi la nécessité d'une affectation manuelle dans une fermeture d'évier.

class SettingsViewModel: ObservableObject {
 @Published var isDarkMode: Bool = false
}

// In your view controller
let viewModel = SettingsViewModel()
NotificationCenter.default.publisher(for: .darkModeChanged)
 .compactMap { $0.userInfo?["enabled"] as? Bool }
 .assign(to: \.isDarkMode, on: viewModel)
 .store(in: &cancellables)

Ce modèle maintient votre code concis et déclaratif. Cependant, soyez prudent avec lorsque le propriétaire de la propriété pourrait être passé avant la fin de l'abonnement. Dans de tels cas, utilisez des références faibles ou l'approche avec la gestion explicite du cycle de vie.

Gestion de la mémoire et cycle de vie

Combiner les abonnements est une ressource qui doit être gérée avec soin pour éviter les fuites de mémoire ou le comportement inattendu. Chaque appel à ou retourne un jeton . Si vous jetez ce jeton sans le stocker, l'abonnement est immédiatement annulé et aucune valeur n'est reçue.

Le motif recommandé est de stocker tous les jetons pouvant être placés dans une propriété sur l'objet propriétaire (comme un contrôleur de vue, un modèle de vue ou un gestionnaire). Lorsque cet objet est délocalisé, le jeu est déinitialisé et tous les abonnements sont automatiquement annulés.

class MyViewController: UIViewController {
 private var cancellables = Set<AnyCancellable>()

 override func viewDidLoad() {
 super.viewDidLoad()
 NotificationCenter.default.publisher(for: .someNotification)
 .sink { [weak self] notification in
 self?.handleNotification(notification)
 }
 .store(in: &cancellables)
 }
}

Une subtilité est que les fermetures au sein des abonnés capturent fortement les références par défaut. Pour éviter de conserver des cycles, capturez lorsque la fermeture fait référence à l'objet propriétaire. Ceci est particulièrement important dans les abonnements de longue durée qui peuvent survivre à l'objet.

Intégration Combiner avec SwiftUI et UIKit

Combiner les travaux en douceur avec SwiftUI et UIKit, bien que les modèles d'intégration diffèrent légèrement.

SwiftUI

SwiftUI a été conçu en tenant compte de Combine. L'emballage de propriétés sur crée automatiquement un éditeur, et les vues de SwiftUI s'abonnent aux modifications via les emballages de propriétés ou . Cela élimine la nécessité de gérer manuellement les abonnements dans de nombreux cas.

class TimerViewModel: ObservableObject {
 @Published var timeString: String = ""
 private var cancellables = Set<AnyCancellable>()

 init() {
 Timer.publish(every: 1.0, on: .main, in: .common)
 .autoconnect()
 .map { formatter.string(from: $0) }
 .assign(to: &$timeString)
 }
}

struct TimerView: View {
 @StateObject private var viewModel = TimerViewModel()

 var body: some View {
 Text(viewModel.timeString)
 }
}

Ici, l'opérateur lie le temps formaté directement à la propriété publiée, et SwiftUI met automatiquement à jour la vue chaque fois que la chaîne change.

UIKit

Dans UIKit, Combine brille au remplacement des motifs de délégation ou de rappel. Vous pouvez utiliser pour réagir aux événements de l'éditeur et mettre à jour les éléments d'interface utilisateur directement, ou utiliser pour les liaisons. La clé est d'envoyer le travail sur la file principale en utilisant avant de mettre à jour l'interface.

class LoginViewController: UIViewController {
 @IBOutlet weak var usernameField: UITextField!
 @IBOutlet weak var passwordField: UITextField!
 @IBOutlet weak var loginButton: UIButton!
 private var cancellables = Set<AnyCancellable>()

 override func viewDidLoad() {
 super.viewDidLoad()
 let usernamePublisher = NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: usernameField)
 .compactMap { ($0.object as? UITextField)?.text }
 let passwordPublisher = NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: passwordField)
 .compactMap { ($0.object as? UITextField)?.text }

 Publishers.CombineLatest(usernamePublisher, passwordPublisher)
 .map { username, password in
 !username.isEmpty && !password.isEmpty
 }
 .assign(to: \.isEnabled, on: loginButton)
 .store(in: &cancellables)
 }
}

Dans cet exemple, Combine écoute les changements de champ texte, combine les dernières valeurs et permet le bouton de connexion uniquement lorsque les deux champs ne sont pas vides. Le résultat est un remplacement propre et déclaratif pour les méthodes de délégation et les modèles d'action cible.

Sujets avancés : Éditeurs personnalisés et contrepression

Pour des besoins plus spécialisés, vous pouvez créer des éditeurs personnalisés en se conformant au protocole . Cela nécessite la mise en œuvre de la méthode , qui relie votre éditeur à un abonné en aval. Bien que la construction d'éditeurs personnalisés offre une flexibilité ultime, la plupart des cas d'utilisation sont couverts par les éditeurs et opérateurs intégrés d'Apple.

Un concept qui mérite l'attention est backpression. Combiner est un cadre impulsé : les abonnés demandent un certain nombre de valeurs à l'éditeur, et l'éditeur les livre au tarif demandé. Cela empêche l'éditeur de surpasser l'abonné. Par défaut, demande un nombre illimité de valeurs, mais vous pouvez implémenter des abonnés personnalisés avec contrôle de la demande si nécessaire.

Pour plus de détails sur les modèles avancés de Combine, consultez WWDC 2019 Session 722: Combiner en pratique pour obtenir des conseils officiels sur la construction de pipelines réactifs.

Avantages de l'adoption de combiner

Intégrer Combine dans votre workflow de développement offre plusieurs avantages tangibles:

  • Clarté de la déclaration[ – Les pipelines sont exprimés en chaîne d'opérateurs, ce qui rend le flux de données évident en un coup d'oeil.
  • Composabilité – De petits opérateurs ciblés peuvent être combinés pour construire un comportement complexe sans nids de rappels enchevêtrés.
  • Type safety – Contrôles du temps de compilation capturent les types de sortie mal appariés et les lacunes de traitement des erreurs avant l'exécution.
  • Gestion des ressources[ – L'annulation automatique sur la distribution réduit les fuites de mémoire.
  • Intégration des écosystèmes – Combiner des œuvres nativement avec SwiftUI, Foundation et de nombreux cadres tiers qui exposent les éditeurs.
  • Testabilité – Les éditeurs peuvent être musqués et les abonnements peuvent être validés en utilisant les attentes de XCTest.

Les équipes qui adoptent Combinez souvent rapporter une réduction du code de plaque de chaudière et moins de bugs liés aux conditions de course ou aux rappels oubliés. Le paradigme réactif encourage également une architecture plus axée sur les données, où le flux d'information est explicite et traçable.

Code des meilleures pratiques de production

Pour tirer le meilleur parti de Combiner tout en évitant les pièges communs, suivez ces lignes directrices :

  • Gérez toujours les abonnements – Stockez les cancellables dans un ensemble et utilisez . Ne jamais compter sur des abonnements de longue durée sans propriétaire clair.
  • Capturer des références faibles – Utiliser dans les fermetures d'évier pour empêcher les cycles de rétention, surtout lorsque la fermeture fait référence à l'objet qui possède les cancellables.
  • Afficher la file d'attente correcte – Utilisez pour s'assurer que les valeurs sont traitées sur le planificateur approprié, généralement la file d'attente principale pour les mises à jour de l'interface utilisateur.
  • Préférence pour les reliures – Lorsque vous avez simplement besoin de mettre à jour une propriété, utilisez au lieu de pour le code de nettoyage.
  • Leverage avec SwiftUI – Dans les projets SwiftUI, combiner les propriétés avec le système d'observation de SwiftUI pour les mises à jour automatiques.
  • Erreurs de poignée au niveau du pipeline[ – Utiliser , ou pour récupérer des défaillances plutôt que de laisser les erreurs se propager aux abonnés.
  • Continuez les pipelines concentrés – Évitez de construire des chaînes monolithiques.
  • Profil avec Instruments – Utilisez le système de nettoyage des fils et des instruments spécifiques à Combiner pour détecter les courses de données et les fuites d'abonnement.

Pour une plongée plus profonde dans Combiner les modèles de manipulation d'erreurs, voir Guide d'Apple sur la manipulation d'erreurs dans Combine.

Conclusion

Combine fournit une base solide et déclarative pour gérer les flux de données asynchrones dans les applications iOS, macOS, watchOS et tvOS. Son modèle éditeur-abonné-opérateur vous permet d'exprimer des flux réactifs complexes avec clarté et précision, tandis que son intégration étroite avec SwiftUI et Foundation réduit la plaque de chaudière et améliore la maintenance du code. Des requêtes réseau simples aux pipelines multisources sophistiqués, Combine équipe les développeurs avec les outils pour construire des applications réceptives et résistantes qui traitent avec grâce les incertitudes des données asynchrones.

En adoptant Combine, commencez par remplacer les schémas de délégation ou de rappel simples par des pipelines simples, puis explorez progressivement des opérateurs plus avancés à mesure que votre confiance augmente. La composabilité du cadre signifie que même une utilisation modeste de Combine peut donner des améliorations immédiates dans la lisibilité et la justesse du code. Avec une attention particulière à la gestion de la mémoire et au filetage, Combine deviendra une partie indispensable de votre boîte à outils de développement iOS, vous permettant de gérer le flux d'événements basés sur le temps avec confiance et élégance.