Pour qu’une ligne de liste SwiftUI reflète les modifications apportées à un élément ObservableObject, transmettez cet élément à une vue de ligne avec @ObservedObject et publiez ses propriétés modifiables. Ce guide crée un petit éditeur d’étiquettes pour Mac et explique les rôles distincts des mises à jour de la collection, de l’observation des lignes et de l’identité.
Pourquoi la ligne ne change-t-elle pas lorsque je modifie un élément ?
Considérez un magasin doté d’une propriété @Published var items: [LabelItem], où chaque LabelItem est une classe. L’ajout d’un élément modifie le tableau. L’attribution d’un nouveau titre à un élément existant modifie l’objet référencé. Il s’agit de mutations différentes : les tableaux Swift sont des types valeur, tandis que les instances de classe ont une sémantique de référence. Un tableau de références à des classes contient toujours des références vers des instances partagées. Types valeur et types référence en Swift.
En pratique, la publication du tableau n’entraîne pas automatiquement l’observation de chaque propriété de chaque objet qu’il contient. Le guide de migration d’Apple présente le suivi des collections d’objets comme un avantage du nouveau système Observation. Dans une implémentation existante basée sur ObservableObject, reliez chaque ligne à l’objet qu’elle affiche. Migrer du protocole Observable Object vers la macro Observable.
Pour diagnostiquer le problème, il est utile de distinguer deux responsabilités : la vue conteneur observe quels éléments appartiennent à la liste ; chaque ligne observe le contenu modifiable de son élément. Apple décrit @ObservedObject comme le wrapper destiné à un ObservableObject reçu en entrée, dont les changements publiés doivent mettre à jour la vue destinataire. ObservedObject.
Créer un exemple macOS complet avec une ligne observée
Utilisez cette version si votre application utilise déjà ObservableObject de Combine, notamment si elle prend en charge des versions de macOS antérieures à 14. L’exemple nécessite macOS 11 ou une version ultérieure, car son propriétaire utilise @StateObject, qu’Apple indique comme disponible à partir de macOS 11. Utilisez une version actuelle du compilateur Swift de Xcode. StateObject.
Créez une cible d’application macOS avec l’interface SwiftUI. Remplacez le fichier d’entrée d’application généré par le programme complet ci-dessous et supprimez la vue de contenu générée si vous n’en avez plus besoin. La cible doit contenir exactement une déclaration @main. Toutes les étiquettes de l’exemple sont fictives et toutes les interactions se déroulent dans la fenêtre de l’application.
import SwiftUI
import Combine
import Foundation
@MainActor
final class LabelItem: ObservableObject, Identifiable {
let id = UUID()
@Published var title: String
@Published var isEnabled: Bool
init(title: String, isEnabled: Bool = true) {
self.title = title
self.isEnabled = isEnabled
}
}
@MainActor
final class LabelStore: ObservableObject {
@Published var items: [LabelItem] = [
LabelItem(title: "Amber"),
LabelItem(title: "Cobalt", isEnabled: false)
]
func reviseFirstItem() {
guard let first = items.first else { return }
first.title = first.title == "Amber" ? "Copper" : "Amber"
first.isEnabled.toggle()
}
func addItem() {
items.append(LabelItem(title: "New label"))
}
func removeLastItem() {
guard !items.isEmpty else { return }
items.removeLast()
}
}
@MainActor
struct LabelRow: View {
@ObservedObject var item: LabelItem
var body: some View {
HStack {
TextField("Label", text: $item.title)
.textFieldStyle(.roundedBorder)
.frame(minWidth: 120)
Text(item.title)
.frame(minWidth: 100, alignment: .leading)
Toggle("Enabled", isOn: $item.isEnabled)
}
.padding(.vertical, 4)
}
}
@MainActor
struct LabelListView: View {
@StateObject private var store = LabelStore()
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Button("Change first label") {
store.reviseFirstItem()
}
.disabled(store.items.isEmpty)
Button("Add label") {
store.addItem()
}
Button("Remove last") {
store.removeLastItem()
}
.disabled(store.items.isEmpty)
}
Text("Rows: \(store.items.count)")
List(store.items) { item in
LabelRow(item: item)
}
}
.padding()
.frame(minWidth: 540, minHeight: 300)
}
}
@main
@MainActor
struct LabelRowsApp: App {
var body: some Scene {
WindowGroup {
LabelListView()
}
}
}
Le magasin possède les instances des éléments. LabelListView crée et conserve le magasin avec @StateObject ; les lignes reçoivent les instances existantes avec @ObservedObject. Apple explique qu’un objet d’état persiste pendant toute la durée de vie de l’identité de son conteneur déclarant, et qu’un objet observé devrait normalement être une entrée plutôt qu’une valeur par défaut créée sur place. StateObject, ObservedObject.
Le champ de texte et le commutateur utilisent des liaisons vers les propriétés publiées de l’élément. Le texte adjacent affiche le même titre, ce qui permet de vérifier visuellement que la modification et l’affichage utilisent le même modèle. @MainActor maintient les opérations du modèle et de l’interface de cet exemple sur l’acteur principal, dont Apple décrit l’exécuteur comme équivalent à la file principale de dispatch. ObservedObject, MainActor.
Ce programme a été examiné à la lumière des API documentées, mais n’a pas été compilé ici : l’environnement disponible ne dispose pas de la chaîne d’outils Swift. Compilez-le et exécutez-le dans Xcode pour vérifier son comportement avec votre cible de déploiement.
Vérifier séparément les modifications d’un élément et celles de la collection
Suivez les étapes ci-dessous pour distinguer les deux chemins de mise à jour. Il s’agit des résultats attendus d’après le programme, et non de mesures de test rapportées.
- Lancez l’application. Elle doit afficher deux lignes, intitulées initialement
AmberetCobalt. - Cliquez sur
Change first label. La première ligne doit afficherCopperet son commutateurEnableddoit changer d’état. Le nombre de lignes doit rester égal à deux. - Modifiez le champ
Labelde la première ligne. Le texte adjacent doit refléter le même titre. - Cliquez sur
Add label. Une troisième ligne nomméeNew labeldoit apparaître et le nombre de lignes doit augmenter. - Cliquez sur
Remove lastjusqu’à ce que la liste soit vide. Les boutons de suppression et de modification de la première étiquette doivent être désactivés ; l’ajout d’une étiquette doit rester possible.
La deuxième étape est particulièrement utile, car la mutation provient de l’action d’un bouton dans la vue conteneur. Elle modifie l’élément existant sans remplacer le tableau. La mise à jour réussie de la ligne vérifie donc directement le chemin d’observation de l’élément. Les étapes d’insertion et de suppression testent séparément le tableau publié du magasin.
Si l’insertion fonctionne, mais pas les changements de titre, examinez la déclaration de propriété dans la ligne qui lit le titre. Dans cette architecture, elle doit recevoir l’élément via @ObservedObject, et le titre modifiable doit être @Published. La publication est attachée à la propriété portant ce wrapper ; elle ne s’applique pas récursivement à tout un graphe d’objets. Published, ObservedObject.
Diagnostiquer les autres problèmes dans un ordre utile
Vérifier que l’action et la ligne partagent la même instance
Suivez l’objet transmis à LabelRow(item:) et celui que l’action modifie. Créer un autre LabelItem avec le même titre crée une instance distincte. De même, copier le titre dans l’état local de la ligne crée une autre valeur qu’il faut synchroniser. Pour cet éditeur, liez directement les contrôles à l’élément partagé, sauf si un processus explicite de brouillon et de validation est nécessaire. Cela découle de la distinction documentée en Swift entre la copie de valeurs et le partage de références vers des classes. Types valeur et types référence en Swift.
Maintenir une identité stable pendant les changements de contenu
L’exemple attribue à chaque élément un identifiant stocké et immuable. ForEach, qui sous-tend la construction dynamique courante de listes, exige des éléments identifiables ou un identifiant explicite. L’identité permet à SwiftUI d’associer le contenu généré à l’élément de données correspondant lors des mises à jour. Elle ne remplace pas la connexion d’observation de la ligne. ForEach.
Évitez d’ajouter un .id(UUID()) susceptible de changer comme mécanisme d’actualisation systématique. Apple documente que le changement de l’identité d’une vue réinitialise son état, y compris l’état du focus. Recréer une ligne pour afficher un titre modifié ajoute un changement de cycle de vie à une modification de contenu ; établissez d’abord le chemin d’observation. StateObject.
Traiter les récapitulatifs du parent comme des consommateurs supplémentaires
Supposons que vous ajoutiez au-dessus de la liste un décompte des éléments activés. Avec l’ancienne conception présentée ici, l’observation de LabelStore ne fait pas automatiquement observer au parent le publisher isEnabled de chaque élément. Corriger les lignes seules n’établit donc pas les dépendances nécessaires à ce récapitulatif.
Choisissez explicitement sa conception de mise à jour : agrégez les notifications des éléments enfants dans le magasin, utilisez des éléments de type valeur dont les modifications sont répercutées dans la collection publiée, ou adoptez Observation si votre cible de déploiement le permet. Ce sont des options architecturales découlant de la distinction entre collection et élément ; l’exemple ne met en œuvre que l’observation directe des lignes. Le guide de migration d’Apple décrit la capacité du nouveau système à suivre les collections et les propriétés effectivement lues par une vue. Migrer du protocole Observable Object vers la macro Observable.
Si vous implémentez une agrégation basée sur les publishers, tenez compte du moment des notifications. Apple précise que @Published publie pendant willSet : l’abonné reçoit la nouvelle valeur avant que la propriété stockée ne change. Relire la propriété dans ce rappel peut donc renvoyer sa valeur précédente. Utilisez la valeur émise pour effectuer le calcul associé à cet événement. Published.
Quand passer à Observation sur macOS 14 ou une version ultérieure ?
Pour une cible de déploiement minimale de macOS 14 ou une version ultérieure, SwiftUI prend en charge le système Observation. Avec un modèle @Observable, SwiftUI suit les lectures de propriétés observables pendant l’évaluation de la vue. Le guide d’Apple présente précisément une collection d’objets observables dont les titres affichés individuellement sont mis à jour lorsque ces titres changent. Gérer les données de modèle dans votre application.
Pour migrer cet exemple, faites utiliser @Observable à LabelItem et à LabelStore, avec import Observation, supprimez leurs conformances à ObservableObject ainsi que @Published. Remplacez @StateObject par @State pour la propriété du magasin propriétaire. Comme cette ligne contient des contrôles modifiables, remplacez son wrapper d’élément entrant @ObservedObject par @Bindable. Ces changements suivent le parcours de migration documenté par Apple. Migrer du protocole Observable Object vers la macro Observable.
Une ligne en lecture seule peut recevoir un élément observable par une simple propriété stockée et lire son titre dans body. @Bindable fournit les liaisons nécessaires aux contrôles comme TextField et Toggle ; il a un rôle différent de @ObservedObject. N’utilisez pas @ObservedObject pour envelopper un modèle qui utilise uniquement le nouveau système Observation : sa contrainte générique exige ObservableObject. Bindable, ObservedObject.
Le choix est simple : conservez la solution de ligne observée pour réparer un modèle Combine existant avec sa plage actuelle de versions de déploiement ; envisagez Observation si macOS 14 est une version minimale acceptable et si le suivi des dépendances au niveau des propriétés convient au reste de l’interface.
Que change l’intégration d’une vue texte AppKit dans la ligne ?
L’exemple SwiftUI utilise des contrôles standard et ne nécessite aucune intégration TextKit personnalisée. Si votre propre ligne encapsule déjà une vue AppKit avec NSViewRepresentable, vérifiez une deuxième frontière après avoir confirmé l’observation du modèle : Apple vous demande de mettre à jour la configuration de la vue AppKit dans updateNSView(_:context:) lorsque l’état SwiftUI correspondant change. Créer la vue native une seule fois ne met pas en œuvre les mises à jour de configuration ultérieures. updateNSView(_:context:).
Pour un NSTextView intégré, distinguez cette synchronisation du modèle vers la vue de la mise en page du texte. Apple décrit NSTextView comme l’interface frontale du système de texte AppKit et documente un commutateur de compatibilité lorsque sa propriété layoutManager est explicitement consultée sur macOS 12 ou une version ultérieure. Évitez d’introduire des changements au moteur de mise en page lorsque vous diagnostiquez une connexion d’observation de ligne manquante. NSTextView.
Pour l’éditeur d’étiquettes, la correction concrète s’effectue à la frontière de la ligne : conservez le magasin, publiez les propriétés modifiables des éléments et observez chaque élément reçu là où il est affiché. Vérifiez une mutation d’élément sans modifier la collection, puis vérifiez l’insertion et la suppression. Cette séquence indique clairement quelle connexion doit être examinée.
Sources
- Types valeur et types référence en Swift — Swift.org
- ObservedObject — Documentation Apple Developer
- StateObject — Documentation Apple Developer
- Published — Documentation Apple Developer
- ForEach — Documentation Apple Developer
- MainActor — Documentation Apple Developer
- Gérer les données de modèle dans votre application — Documentation Apple Developer
- Migrer du protocole Observable Object vers la macro Observable — Documentation Apple Developer
- Bindable — Documentation Apple Developer
- updateNSView(_:context:) — Documentation Apple Developer
- NSTextView — Documentation Apple Developer