SwiftProgrammationDéveloppeur iOS

Par quel moyen la stratégie d'expansion de macro à la compilation que **Swift** utilise-t-elle pour implémenter le suivi automatique des dépendances du framework **Observation**, et comment ce mécanisme élimine-t-il le code boilerplate manuel requis par **ObservableObject** ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse à la question

Historique de la question

Avant Swift 5.9, la programmation réactive dans SwiftUI s'appuyait sur le protocole ObservableObject combiné avec le framework Combine. Les développeurs annotaient manuellement les propriétés avec @Published pour synthétiser des éditeurs, ou appelaient objectWillChange.send() pour notifier les vues des mutations. Ce modèle souffrait de mises à jour à granulation grossière : tout changement de propriété déclenchait une réévaluation complète du corps de la vue — et imposait des sémantiques de référence, empêchant l'utilisation de structures pour des modèles de vue complexes. Le framework Observation a été introduit pour fournir une réactivité automatique et à granulation fine sans déclarations explicites d'éditeurs.

Le problème

Le défi principal était de détecter l'accès et la mutation des propriétés sans boilerplate explicite tout en maintenant la sécurité de type et une haute performance. Les solutions traditionnelles nécessitaient soit de l'observation manuelle de clé-valeur (KVO), qui est faiblement typée et fragile, soit des types de wrapper qui encombraient le modèle de domaine. Le système devait intercepter les opérations de lecture et d'écriture pour enregistrer les dépendances dynamiquement, mais sans le surcoût d'exécution du swizzling de méthode ou des contraintes architecturales de la propagation de l'identité comptée par référence de ObservableObject.

La solution

Swift utilise la macro @Observable, qui combine des capacités de macro de pair, de membre et d'accessoire. Pendant la compilation, la macro transforme la classe annotée en injectant une instance privée de ObservationRegistrar. Elle réécrit ensuite chaque propriété stockée en accessoirs calculés qui enveloppent les lectures avec _$observationRegistrar.track(self, keyPath: ...) et les écritures avec des notifications willSet/didSet. Cette expansion synthétise automatiquement la conformité au protocole Observable et implémente la propriété calculée observationRegistrar requise. SwiftUI s'intègre à ce registraire lors de l'évaluation du corps de la vue, s'enregistrant comme observateur uniquement pour les propriétés réellement accédées, réalisant ainsi des mises à jour granulaires sans configuration manuelle de Combine.

@Observable class SettingsViewModel { var isDarkModeEnabled = false var notificationCount = 0 } // Expansion conceptuelle du compilateur : class SettingsViewModel { private let _$observationRegistrar = ObservationRegistrar() var isDarkModeEnabled: Bool { get { _$observationRegistrar.track(self, keyPath: \.isDarkModeEnabled) return _isDarkModeEnabled } set { _$observationRegistrar.willSet(self, keyPath: \.isDarkModeEnabled) let oldValue = _isDarkModeEnabled _isDarkModeEnabled = newValue _$observationRegistrar.didSet(self, keyPath: \.isDarkModeEnabled, oldValue: oldValue) } } private var _isDarkModeEnabled = false // ... modèle identique pour notificationCount }

Situation de la vie réelle

Vous êtes architecte d'une application de tableau de bord financier en temps réel SwiftUI affichant des prix d'actions en direct, des totaux de portefeuille utilisateur et des fils d'actualités. Le modèle de vue contient trente propriétés distinctes, allant des indicateurs d'interface utilisateur booléens à des structures de données de graphiques complexes.

Au début, l'équipe a mis cela en œuvre en utilisant ObservableObject avec des wrappers @Published sur chaque propriété. Cela a causé une dégradation sévère de la performance : lorsqu'un seul prix d'actions se mettait à jour, l'ensemble du tableau de bord était recomputé parce que ObservableObject notifie que « quelque chose a changé » dans tout l'objet, sans granularité. Le code était également verbeux, nécessitant des déclarations répétitives de @Published var et un stockage manuel AnyCancellable pour éviter les fuites de mémoire dans les abonnements.

L'équipe a évalué trois approches architecturales pour résoudre les problèmes de performance et de boilerplate.

La première approche impliquait une optimisation manuelle de Combine. Ils créeraient des instances individuelles de PassthroughSubject pour chaque propriété critique et s'abonneraient à des mises à jour spécifiques en utilisant .onReceive. L'avantage était un contrôle précis sur quels composants de l'interface utilisateur se rafraîchissaient. Cependant, l'inconvénient était l'énorme encombrement du code — trente sujets nécessitaient trente abonnements et une gestion manuelle de la mémoire sujette aux erreurs avec Set<AnyCancellable>, rendant le code insoutenable.

La deuxième approche proposait d'utiliser @State de SwiftUI avec des modèles de vue de type valeur. Ils traiteraient le modèle de vue comme une valeur immuable et le remplaceraient à chaque mutation. L'avantage était la sémantique des valeurs naturelles et des vérifications d'égalité automatiques évitant des mises à jour redondantes. L'inconvénient était la perte de l'identité de référence ; chaque mutation créait une nouvelle instance, rompant la restauration de la position de ScrollView et rendant impossible la coordination d'objets complexes à cause de la perte d'identité.

La troisième approche adoptait la macro @Observable. En annotant la classe avec @Observable et en supprimant tous les attributs @Published, le compilateur transformait automatiquement les propriétés pour utiliser le ObservationRegistrar. L'avantage était double : la syntaxe restait propre avec de simples déclarations var, et SwiftUI suivait automatiquement les propriétés qui étaient accessibles dans le corps de chaque vue, ne mettant à jour que ces sous-vues spécifiques. L'inconvénient était la nécessité de migrer vers Swift 5.9 et de former l'équipe sur les techniques de débogage des macros.

L'équipe a choisi la troisième approche car elle a éliminé 200 lignes de code d'abonnement Combine tout en résolvant le problème de granularité. Ils ont observé une réduction de 40 % de l'utilisation du processeur lors de mises à jour de prix à haute fréquence. Le résultat était un tableau de bord réactif où chaque étiquette de prix d'actions se mettait à jour indépendamment sans déclencher de recalculs de mise en page pour la section d'actualités statiques.

Ce que les candidats manquent souvent

Pourquoi le framework Observation exige-t-il que le type observé soit une classe (sémantiques de référence), et quelle erreur de compilation se produit-elle si @Observable est appliqué à une structure ?

La macro @Observable s'étend en injectant un ObservationRegistrar comme propriété stockée et en implémentant le protocole Observable. Les structs sont des types de valeur avec des sémantiques de copie à l'écriture ; chaque mutation crée conceptuellement une nouvelle instance avec une identité distincte. Le ObservationRegistrar maintient un état interne — listes d'observateurs et portées de suivi — qui doivent persister à travers les mutations pour maintenir le graphe d'observation. Si elle est appliquée à une structure, les mutations copieraient incorrectement l'état du registraire, rompant la connexion entre les observateurs et l'instance. Le compilateur empêche cela en générant une erreur indiquant que la macro ne peut pas ajouter la propriété stockée requise à un type de valeur d'une manière qui satisfait les exigences du protocole Observable pour une identité stable, ou plus spécifiquement, que le type résultant ne peut pas se conformer à Observable en raison du manque de stabilité de référence nécessaire.

Comment le framework Observation gère-t-il les objets observables imbriqués, et pourquoi n'y a-t-il pas de valeur projetée (comme $property) pour des propriétés individuelles comme c'était le cas avec @Published ?

Lorsqu'une classe @Observable contient une propriété qui est elle-même une classe @Observable, le framework suit l'accès au niveau de la propriété, non pas en observant automatiquement de manière récursive les objets imbriqués. L'accès à outer.inner.name enregistre une dépendance sur la propriété inner de l'objet extérieur. Si l'instance inner est entièrement remplacée, les observateurs sont notifiés. Cependant, les modifications apportées à inner.name ne notifient pas les observateurs de l'objet extérieur à moins que l'objet extérieur ne suive explicitement le inner. Contrairement à Combine, il n'existe pas de concept de valeur projetée pour les propriétés individuelles dans Observation car le cadre utilise un suivi direct des propriétés via le registraire plutôt que des flux d'éditeurs. La syntaxe $ dans SwiftUI pour Observation est plutôt utilisée sur l'instance entière lorsqu'elle est enveloppée avec @Bindable (par exemple, @Bindable var viewModel: SettingsViewModel permet $viewModel.isDarkModeEnabled), et non sur des déclarations de propriété individuelles.

Quelles garanties spécifiques de sécurité des threads le framework Observation fournit-il, et comment interagit-il avec les acteurs de concurrence de Swift ?

Le ObservationRegistrar lui-même n'est pas intrinsèquement thread-safe ; il suppose un accès sérialisé à l'objet observable. Lorsqu'une classe @Observable est isolée à un acteur (comme @MainActor), toutes les mutations et observations se produisent automatiquement dans le contexte de cet acteur, empêchant les conflits de données. Le cadre garantit que les rappels d'observation respectent le domaine d'isolement de l'observateur en utilisant des vérifications Sendable. Un détail d'implémentation critique est que le mécanisme de suivi utilise le stockage TaskLocal pour maintenir le champ d'observation actuel lors de l'exécution du corps de la vue. Cela signifie que l'enregistrement des observations est implicitement lié au contexte de la Task actuelle et ne peut pas s'échapper à travers des frontières de concurrence non structurées sans transfert explicite, garantissant que les observations ne sont actives que pendant la transaction asynchrone spécifique où elles ont été enregistrées.