Le vrai coût de SwiftData, c'est la discipline du schéma
Le ShoppingItem de Get Bananas est l’exemple canonique de l’importance de la discipline du schéma dans SwiftData. Le schéma d’origine n’incluait pas d’horodatage lastModified ; l’ajouter par la suite a exigé une forme de migration spécifique, car des données existantes se trouvaient déjà sur disque, et le champ a été rendu optionnel précisément pour corriger un plantage de migration survenu lorsqu’il avait été ajouté la première fois en non optionnel.1
L’API de SwiftData se résume à deux macros. @Model sur une classe en fait un type persistant. @Attribute(.unique) sur une propriété lui confère une contrainte d’unicité. Le framework masque la gestion de la pile de Core Data, la chorégraphie des value-transformers et le boilerplate de NSManagedObjectContext. Ce que le framework ne masque pas, c’est la migration de schéma ; il la rend simplement déclarative plutôt qu’impérative. Le coût de la négligence à l’égard des migrations, c’est le bug qui efface les données d’un utilisateur lors d’une mise à jour de routine.
La thèse : SwiftData est bon marché au démarrage et coûteux à migrer de manière bâclée. La discipline tient au nommage, à l’optionalité et au VersionedSchema dès le premier jour, et non le jour où vous réalisez que vous auriez dû le faire.
TL;DR
- La macro
@Modeltransforme une classe en type SwiftData persistant. Le framework génère le schéma à partir des déclarations de propriétés au moment de la compilation. - Ajouter une nouvelle propriété optionnelle est une migration sans effet : la migration légère de SwiftData s’en charge. Ajouter une propriété non optionnelle à un schéma existant exige un
VersionedSchemaaccompagné d’unMigrationPlanqui indique au framework comment renseigner le nouveau champ pour les lignes existantes. - Le coût de l’omission du
VersionedSchemadès le premier jour, c’est que tout changement de schéma non trivial en v2 risque de supprimer la base de données d’un utilisateur, car le chemin léger est conservateur et abandonne lorsqu’il ne parvient pas à inférer la migration. @Attribute(.unique)est le bon outil pour les clés naturelles (unUUIDque vous avez généré, un identifiant externe que vous avez importé).@Relationshipest le bon outil pour les références parent/enfant. Les deux sont des macros qui génèrent sous le capot la bonne tuyauterie Core Data.2
Ce que fait réellement @Model
Un type SwiftData est une classe Swift à laquelle la macro @Model est appliquée. Le ShoppingItem de Get Bananas en est la forme canonique :
import Foundation
import SwiftData
@Model
final class ShoppingItem {
@Attribute(.unique) var id: UUID
var name: String
var amount: String
var section: String
var isChecked: Bool
var isOptional: Bool
var sortOrder: Int
var lastModified: Date?
init(id: UUID = UUID(), name: String, amount: String, section: String,
isOptional: Bool = false, sortOrder: Int = 0) {
self.id = id
self.name = name
self.amount = amount
self.section = section
self.isChecked = false
self.isOptional = isOptional
self.sortOrder = sortOrder
self.lastModified = Date()
}
}
Trois détails sur cette forme que l’API masque.
@Model n’exige pas de déclaration distincte du schéma du magasin persistant. SwiftData lit la définition de la classe au moment de la compilation et synthétise le schéma. Les propriétés de la classe deviennent les attributs du modèle ; leurs types Swift deviennent les types des colonnes. Il n’y a aucun fichier .xcdatamodeld à maintenir (bien que le NSManagedObjectModel sous-jacent de Core Data existe toujours et soit ce qui soutient le schéma à l’exécution).2
@Attribute(.unique) est une contrainte sur une seule colonne, et non une déclaration de PRIMARY KEY. L’identité persistante de SwiftData est le PersistentIdentifier, généré automatiquement pour chaque ligne. La déclaration @Attribute(.unique) indique au framework « cette colonne stocke au plus une ligne par valeur ». Lorsque vous insérez un modèle avec une valeur .unique qui existe déjà, SwiftData effectue un upsert : la ligne existante est mise à jour plutôt que rejetée. La sémantique compte pour le code produit : .unique n’est pas une validation au niveau de l’UI qui empêche la soumission de doublons ; c’est une garantie de stockage « au plus un » qui fusionne discrètement. Le pattern id: UUID ci-dessus est celui recommandé pour la synchronisation inter-processus (lorsque vous voulez un identifiant stable qui survit à la disparition du PersistentIdentifier en cours de processus), et le comportement d’upsert est exactement ce que vous voulez lorsque le même UUID arrive par deux chemins de synchronisation.
Les classes @Model sont des types par référence, et non des types par valeur. Modifier une propriété sur une instance de ShoppingItem déclenche le suivi des changements de SwiftData ; le framework enregistre le changement et le persiste à la prochaine sauvegarde du contexte. L’intégration SwiftUI via @Query redessine toute vue observant le prédicat correspondant. Le pattern est semblable à @Observable (abordé dans What SwiftUI Is Made Of), avec la persistance superposée.
Les champs optionnels sont la migration bon marché
Le champ lastModified: Date? sur ShoppingItem est optionnel, et cette optionalité est porteuse. Le champ a été ajouté après la sortie de la v1 pour prendre en charge la synchronisation entre appareils et la résolution de conflits ; les lignes existantes sur les appareils des utilisateurs n’avaient aucune valeur lastModified. Un champ optionnel sans valeur par défaut permet à la migration légère de SwiftData de gérer l’ajout sans écrire le moindre code de migration : les lignes existantes obtiennent nil ; les nouvelles lignes obtiennent ce que l’init définit.3
Le chemin de migration légère est le chemin courtois du framework. SwiftData inspecte le nouveau schéma et le magasin persistant, infère le plus petit changement compatible et l’applique. La migration est automatique ; l’utilisateur ne voit rien ; l’application se lance normalement sur les données existantes. Les cas que le chemin léger gère proprement :
- Ajouter une propriété optionnelle
- Supprimer une propriété (les données sont abandonnées ; les lectures existantes ne voient plus la colonne)
- Renommer un attribut que le framework peut faire correspondre par indication (à l’aide de
@Attribute(originalName: ...)) - Renommer une classe
@Modelque le framework peut faire correspondre (à l’aide de@Model.originalNameou d’une indication)
Les cas où le chemin léger abandonne :
- Ajouter une propriété non optionnelle sans valeur par défaut à un schéma existant (les lignes existantes n’ont aucune valeur pour la renseigner)
- Changer le type d’une propriété (par exemple,
Int→String) - Diviser un modèle en deux modèles, ou fusionner deux modèles en un seul
- Tout ce qui exige une logique personnalisée pour migrer
Lorsque le chemin léger abandonne, le comportement sûr consiste à faire échouer la migration. Le comportement dangereux serait de supprimer la base de données et de repartir de zéro ; le framework est conservateur et refuse de le faire silencieusement. L’utilisateur voit l’application planter au lancement avec une erreur de migration ; le développeur voit une stack trace pointant vers la discordance de schéma ; personne ne perd de données, mais tout le monde perd confiance.
Le coût de l’omission du VersionedSchema dès le premier jour se manifeste à la frontière v2 → v3, lorsque vous ajoutez la troisième fonctionnalité dont le changement de schéma dépasse ce que le chemin léger gère.
VersionedSchema et MigrationPlan : la discipline du premier jour
VersionedSchema déclare une version spécifique du schéma du modèle. MigrationPlan déclare comment migrer d’une version à la suivante.4 La forme :
import SwiftData
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] = [ShoppingItemV1.self]
}
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] = [ShoppingItemV2.self]
}
enum AppMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] = [
SchemaV1.self,
SchemaV2.self,
]
static var stages: [MigrationStage] = [
MigrationStage.lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self)
]
}
Les classes de modèle elles-mêmes passent dans l’espace de noms du schéma versionné :
extension SchemaV1 {
@Model
final class ShoppingItemV1 { /* v1 fields */ }
}
extension SchemaV2 {
@Model
final class ShoppingItemV2 { /* v2 fields, including lastModified */ }
}
Le ModelContainer est construit avec le plan de migration :
let container = try ModelContainer(
for: ShoppingItemV2.self,
migrationPlan: AppMigrationPlan.self,
configurations: ModelConfiguration("ShoppingList")
)
Le plan de migration fournit au framework un graphe typé de l’évolution du schéma. Lorsque l’application en v2 se lance contre une base de données en v1, le framework parcourt le plan de migration, applique les étapes nommées et amène la base de données à la v2. Lorsque vous publiez la v3, vous ajoutez SchemaV3.self à schemas et un nouveau MigrationStage entre la v2 et la v3.
La discipline consiste à livrer VersionedSchema en v1, même lorsqu’il n’y a qu’une seule version. Le coût en est un fichier supplémentaire et une déclaration enum supplémentaire. Le coût de ne pas le faire, c’est que le premier changement de schéma non trivial de la v2 exige d’envelopper rétroactivement la v1 dans un VersionedSchema, ce qui est faisable mais demande du soin pour reproduire la forme exacte de la v1 afin que le framework puisse identifier les données existantes comme étant SchemaV1. Le vous-futur travaillant sur la v2 paiera la taxe ; le vous-présent peut la payer une fois et l’oublier.
MigrationStage personnalisé pour les cas difficiles
Les migrations légères couvrent la plupart des changements additifs. Les changements de type, les divisions, les fusions et les renseignements conditionnels nécessitent un MigrationStage.custom :
static var stages: [MigrationStage] = [
MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
// Read v1 rows; stage any derived state to a transient store
// (UserDefaults / temp file) since the v1 and v2 contexts do
// not share state, and didMigrate cannot read v1.
let v1Items = try context.fetch(FetchDescriptor<ShoppingItemV1>())
stageDerivedState(from: v1Items)
},
didMigrate: { context in
// Populate v2-only fields on existing rows
let v2Items = try context.fetch(FetchDescriptor<ShoppingItemV2>())
for item in v2Items where item.lastModified == nil {
item.lastModified = Date()
}
try context.save()
}
)
]
Les deux closures se déclenchent avant et après que le framework applique la migration structurelle. willMigrate s’exécute contre le schéma v1 ; didMigrate s’exécute contre le schéma v2. Le corps de la closure est du code SwiftData normal (fetch descriptors, sauvegardes de model context, les mêmes API utilisées dans l’application en cours d’exécution), opérant contre un contexte transitoire de migration.
Le pattern qui survit à la production consiste à garder willMigrate vide et à placer toute la logique de renseignement dans didMigrate. Lire les données v1 à l’intérieur de willMigrate est autorisé, mais le schéma v2 n’existe pas encore du point de vue du framework ; tout calcul doit donc être préparé dans un magasin transitoire que la closure didMigrate peut lire. La règle plus simple : les migrations structurelles sont le travail du framework ; renseigner les champs propres à la v2 sur les lignes existantes est le travail de didMigrate.
Quand @Attribute et @Relationship méritent leur nom
Deux macros effectuent l’essentiel du travail de décoration de schéma dans les classes @Model.
@Attribute décore une propriété unique d’une contrainte ou d’une indication :
@Attribute(.unique)impose l’unicité, comme dansShoppingItem.id@Attribute(.externalStorage)stocke les gros blobs deDataen dehors de la base de données (données d’image, tampons audio)@Attribute(originalName: "old_field_name")fait correspondre une propriété à une colonne renommée lors de la migration@Attribute(.transformable(by: ...))applique unValueTransformerà un type non Codable
La bonne discipline : utilisez .unique pour les champs qui devraient véritablement être uniques (un UUID que vous avez généré, un identifiant externe), utilisez .externalStorage pour tout blob de plus de quelques Ko, utilisez originalName lorsqu’un renommage en v2 d’une propriété perdrait sinon les données v1.
@Relationship décore une propriété qui pointe vers une autre classe @Model ou vers une collection de celles-ci :
@Model
final class List {
var name: String
@Relationship(deleteRule: .cascade, inverse: \ShoppingItem.list)
var items: [ShoppingItem] = []
}
@Model
final class ShoppingItem {
var name: String
var list: List?
}
Le deleteRule: .cascade signifie que la suppression du List parent supprime toutes les lignes ShoppingItem enfants. Le paramètre inverse: indique au framework quelle propriété de l’enfant pointe vers le parent ; le framework s’en sert pour une maintenance bidirectionnelle prévisible. SwiftData peut parfois inférer l’inverse automatiquement, et inverse: nil est pris en charge pour les relations explicitement unidirectionnelles, mais la valeur par défaut sûre consiste à déclarer inverse: chaque fois que l’inférence serait ambiguë.5
La bonne discipline : déclarez les relations avec un deleteRule explicite (la valeur par défaut est .nullify, ce qui correspond rarement à ce que vous voulez) et déclarez inverse: chaque fois que la relation est bidirectionnelle (plutôt que de vous fier à l’inférence du framework). Les valeurs par défaut implicites sont généralement fausses ; la forme explicite est un paramètre supplémentaire et un bug évité pour toujours.
Franchir une frontière d’acteur : envoyez l’identifiant, pas le graphe
Une classe @Model n’est pas Sendable, et le bon réflexe est d’arrêter d’essayer de l’y forcer. L’instance est une référence dans un graphe d’objets vivant détenu par un ModelContext ; le framework ne peut pas garantir que ce graphe est sûr à lire depuis un autre acteur, le type est donc délibérément laissé non Sendable. Forcer la conformité ne fait pas disparaître la situation de compétition de données ; elle la masque.7
Le pattern qui fonctionne consiste à envoyer l’identité et les valeurs simples, puis à re-récupérer de l’autre côté. PersistentIdentifier est Sendable, il franchit donc la frontière proprement. Extrayez toutes les valeurs scalaires dont la destination a besoin (un nom, un drapeau, un delta dans une petite struct), transmettez-les avec l’identifiant, et faites en sorte que l’acteur récepteur re-récupère le modèle depuis son propre contexte à l’aide de l’identifiant :
// On the source actor: extract identity + plain values, never the model.
let id: PersistentIdentifier = item.persistentModelID
let snapshot = ItemSnapshot(name: item.name, isChecked: item.isChecked)
// On the destination actor: re-fetch from this context, then mutate.
let fetched = destinationContext.model(for: id) as? ShoppingItem
Le mode d’échec à éviter consiste à transmettre le graphe du modèle lui-même. Lorsqu’une partie du graphe franchit la frontière, le récepteur obtient un modèle qui s’hydrate partiellement de l’autre côté : les relations et les propriétés chargées paresseusement qui n’ont jamais été résolues (faulted in) sur le contexte source se résolvent contre le mauvais contexte (ou pas du tout), et les bugs qui s’ensuivent sont du genre silencieux. L’identifiant accompagné des valeurs extraites est le contrat sûr ; le graphe ne l’est pas. Un ModelActor encapsule cette discipline en possédant un contexte et en distribuant des valeurs plutôt que des instances.7
La synchronisation CloudKit et le piège de l’entitlement d’app group
Déplacer un magasin SwiftData dans un conteneur d’app group pour qu’un widget ou une extension puisse le lire interagit avec la synchronisation CloudKit d’une manière qui mord les applications après leur sortie. Deux faits font découler le reste.
Premièrement, l’emplacement du magasin. Avec le ModelConfiguration par défaut, SwiftData copie pour vous le magasin existant dans le conteneur d’app group lorsqu’une application évolue de l’absence de groupe vers un app group ; la formulation d’Apple est que SwiftData « copie le magasin existant dans le conteneur d’app group ».8 Avec une URL de magasin personnalisée, vous possédez l’emplacement : vous copiez le fichier dans le nouveau conteneur et y pointez la configuration vous-même. Le chemin par défaut est le chemin commode précisément parce que le framework effectue la copie ; le chemin personnalisé échange cette commodité contre le contrôle.
Deuxièmement, l’entitlement. Chaque membre de l’app group qui lit un magasin synchronisé via CloudKit doit porter le même entitlement CloudKit, car chacun de ces processus synchronisera ce conteneur pour son propre compte. Cette exigence est le piège : un widget ou une extension n’a ni le budget d’exécution ni la fenêtre au premier plan pour piloter une synchronisation verbeuse, et lui confier l’entitlement CloudKit le force à essayer. Le correctif consiste à diviser en deux instances de ModelConfiguration : un magasin synchronisé (entitlement CloudKit, possédé par l’application principale) et un magasin local dans le conteneur d’app group que le widget et les extensions lisent sans jamais synchroniser. Placez la synchronisation là où une application au premier plan peut bien la réaliser, et gardez les données partagées-pour-lecture hors du chemin de synchronisation.8
Ce que je construirais différemment
Trois patterns que les applications du cluster livrent, ou auraient souhaité livrer.
Livrez VersionedSchema dès la v1. Chaque classe @Model livrée devrait vivre à l’intérieur d’un VersionedSchema dès le premier jour. Le coût en est un enum d’enveloppe par version de schéma. L’avantage, c’est que le premier changement non trivial de la v2 est un ajout d’une seule ligne à MigrationPlan.schemas au lieu d’un refactoring rétroactif de deux jours.
Rendez chaque horodatage optionnel. Les champs comme lastModified, createdAt et updatedAt qui existent pour la synchronisation entre appareils ou la résolution de conflits devraient être optionnels en v1 si le produit v1 n’en a pas besoin. L’optionalité maintient bon marché la migration vers la v2 (lorsque vous en avez bel et bien besoin). Les renseigner sur les lignes existantes pendant didMigrate est une seule boucle ; les rendre non optionnels dès la v1 est une contrainte qui peut briser le backfill sur les données des utilisateurs.
Utilisez des UUID comme clé naturelle, pas le PersistentIdentifier. Le PersistentIdentifier de SwiftData est en cours de processus. La synchronisation entre appareils, l’intégration MCP (abordée dans Two Agent Ecosystems, One Shopping List) et toute référence hors processus nécessitent un identifiant stable. Un UUID avec @Attribute(.unique) est la bonne forme ; le PersistentIdentifier en cours de processus est la mauvaise forme pour tout ce qui franchit une frontière de processus.
Quand @Model est la mauvaise réponse
Trois cas où SwiftData n’est pas le bon outil :
État clé/valeur à enregistrement unique. Les réglages de l’application, la langue sélectionnée par l’utilisateur, l’horodatage de la dernière synchronisation. Utilisez UserDefaults ou NSUbiquitousKeyValueStore (abordés dans Five Apple Platforms, Three Shared Files). La surcharge de SwiftData pour une seule ligne est un cérémonial gaspillé ; les magasins clé/valeur sont le bon substrat.
Données faisant autorité côté serveur sans écritures hors ligne. Une liste récupérée depuis une API REST et affichée en lecture seule. SwiftData est excessif si la source de vérité est le serveur et que le cache local n’est qu’un cache. Un simple instantané Codable dans Documents/ plus un tableau mis en cache en mémoire suffit ; la taxe de migration de SwiftData ne vaut pas la peine d’être payée si les données ne survivent pas à une réinitialisation matérielle.
Coordination multi-processus. SwiftData opère à l’intérieur d’un processus. Un serveur MCP s’exécutant en dehors de l’application iOS ne peut ni lire ni écrire le conteneur SwiftData de l’application. L’état inter-processus nécessite une forme différente : un fichier JSON iCloud Drive, un conteneur d’App Group partagé, ou une couche de synchronisation explicite qui fait le pont entre les processus. (Get Bananas associe SwiftData à du JSON iCloud Drive pour exactement cette raison.)6
Les données sont de gros blobs qui changent rarement. Un fichier audio de 10 Mo, un jeu de données d’images de 50 Mo. Utilisez @Attribute(.externalStorage) si les blobs sont à l’intérieur des lignes SwiftData ; sinon, utilisez directement le système de fichiers avec, dans SwiftData, des métadonnées pointant vers des URL de fichiers.
Ce que ce pattern signifie pour les applications livrées sur iOS 26+
Trois enseignements.
-
Les macros sont la partie facile. Les migrations sont le coût.
@Modelet@Attributesont des déclarations de deux lignes qui masquent une grande part de la tuyauterie Core Data. La discipline de migration est ce que vous payez réellement tout au long de la vie de l’application ; concevez la v1 en pensant à la v2. -
VersionedSchemadès le premier jour est non négociable pour les applications livrées. L’enumd’enveloppe est un fichier supplémentaire. Le coût rétroactif de son ajout ultérieur est bien plus élevé. -
Les champs optionnels et les relations explicites sont l’assurance bon marché. Des horodatages optionnels pour les métadonnées de synchronisation, un
deleteRuleet uninverse:explicites sur les relations. Les deux sont de minuscules déclarations qui achètent beaucoup de flexibilité pour la v2.
Le cluster complet Apple Ecosystem : les App Intents typés pour Apple Intelligence ; les serveurs MCP pour les agents inter-LLM ; la question du routage entre eux ; les Foundation Models pour le LLM sur l’appareil et le protocole Tool ; les Live Activities pour la machine à états de l’écran verrouillé sur iOS ; le contrat de runtime watchOS sur l’Apple Watch ; les internes de SwiftUI pour le substrat du framework ; le modèle mental spatial de RealityKit pour les scènes visionOS ; les patterns Liquid Glass pour la couche visuelle ; la livraison multiplateforme pour la portée entre appareils. Le hub se trouve dans la série Apple Ecosystem. Pour un contexte plus large sur iOS avec des agents IA, consultez le guide iOS Agent Development.
FAQ
Quelle est la différence entre @Model et le NSManagedObject de Core Data ?
@Model est une macro Swift qui génère sous le capot la tuyauterie de NSManagedObject. SwiftData utilise Core Data comme magasin de stockage sous-jacent, le modèle d’exécution est donc le même ; la différence tient à la surface. @Model supprime le fichier .xcdatamodeld, le cérémonial des value-transformers et la gestion du cycle de vie de NSManagedObjectContext. Vous obtenez le même magasin persistant avec une API à la forme de Swift.
Ai-je besoin de VersionedSchema si je ne prévois jamais de changer le schéma ?
Si votre application pourrait livrer une v2, oui. S’il s’agit d’une démo ponctuelle, non. Le coût de VersionedSchema dès la v1 est une déclaration enum supplémentaire. Le coût de son ajout rétroactif en v2 consiste à reproduire la forme exacte du schéma v1 pour que le framework reconnaisse les données existantes, ce qui est faisable mais sujet à erreur. La plupart des applications livrées finiront par avoir besoin d’un changement de schéma ; prévoyez-le dans la v1.
Quand devrais-je utiliser @Attribute(.unique) ?
Lorsque le champ est une clé naturelle pour la ligne : un UUID que vous avez généré, un identifiant externe que vous avez importé, un slug que vous avez assigné. SwiftData traite .unique comme un upsert : si vous insérez un modèle dont la valeur .unique existe déjà, la ligne existante est mise à jour plutôt qu’une nouvelle ligne ajoutée. Cette sémantique est ce qui rend sûrs les chemins de synchronisation de style upsert (le même UUID provenant de deux appareils) ; c’est aussi pourquoi .unique est le mauvais outil pour les champs de nom d’affichage comme title, car deux utilisateurs saisissant le même titre fusionneraient silencieusement leurs lignes au lieu de produire deux enregistrements distincts.
Comment gérer un champ non optionnel ajouté à un schéma existant ?
Utilisez un MigrationStage.custom avec une closure didMigrate qui renseigne le champ sur les lignes existantes. Ou, plus simple : déclarez le champ comme optionnel dans la nouvelle version du schéma et renseignez-le paresseusement à l’accès. L’optionalité est la migration la moins coûteuse ; les ajouts non optionnels nécessitent une logique de renseignement explicite.
Qu’est-ce que le PersistentIdentifier par rapport à mon propre UUID ?
PersistentIdentifier est l’ID de ligne en cours de processus de SwiftData ; il est généré automatiquement et survit pendant toute la durée du processus en cours d’exécution. Votre propre UUID avec @Attribute(.unique) est un identifiant stable inter-processus et inter-appareils. Utilisez PersistentIdentifier pour les références en cours de processus à l’intérieur de l’application. Utilisez un UUID pour tout ce qui franchit une frontière de processus (synchronisation entre appareils, intégrations externes, outils MCP, appels réseau).
Références
-
Get Bananas de l’auteur, une application de liste de courses SwiftUI qui associe SwiftData à la synchronisation JSON iCloud Drive et à un serveur MCP. Le modèle
ShoppingItema évolué tout au long du premier cycle de développement ; le champlastModified: Date?a été ajouté après le schéma initial (commit268a00dle 2025-12-01, « Make lastModified optional to fix migration crash ») parce que le rendre non optionnel cassait la migration lorsque les lignes existantes n’avaient aucune valeur pour le renseigner. ↩ -
Apple Developer, « SwiftData » et « Adding and editing persistent data in your app ». La macro
@Model, la surface de contrainte@Attributeet la relation auNSManagedObjectModelde Core Data. ↩↩ -
Apple Developer, « Preserving your app’s model data across launches » et « Adopting SwiftData for a Core Data app ». La sémantique de la migration légère et ce qui déclenche l’abandon du framework. ↩
-
Apple Developer, « VersionedSchema » et « SchemaMigrationPlan ». Les déclarations de schéma versionné, les définitions des étapes de migration et le constructeur de
ModelContainerqui prend un plan de migration. ↩ -
Apple Developer, « Defining data relationships with enumerations and model classes » et « Schema.Relationship ». La macro
@Relationship, les options dedeleteRule(.cascade,.nullify,.deny,.noAction) et le rôle du paramètreinverse:dans la maintenance des relations bidirectionnelles. ↩ -
Analyse de l’auteur dans Two Agent Ecosystems, One Shopping List, 29 avril 2026, et Five Apple Platforms, Three Shared Files. Les patterns de synchronisation inter-processus et inter-appareils de Get Bananas + Return qui complètent (et parfois remplacent) SwiftData au sein d’un workflow multi-processus. ↩
-
Apple Developer, « PersistentIdentifier » (conforme à
Sendable) et « ModelActor ». L’équipe SwiftData a confirmé lors du SwiftData Group Lab de la WWDC 2026 que les objets@Modelne sont pasSendableet ne devraient pas être forcés à s’y conformer, parce qu’ils constituent un graphe de références vivant à l’intérieur d’un contexte ; le contrat de frontière recommandé consiste à transmettre lePersistentIdentifierSendableaccompagné des valeurs simples extraites et à re-récupérer sur le contexte de destination, et transmettre le graphe du modèle laisse le récepteur avec un objet partiellement hydraté. Paraphrasé à partir d’un enregistrement transcrit localement du SwiftData Group Lab de la WWDC 2026 ; Apple ne publie aucun sous-titre officiel pour les labs. ↩↩ -
Apple Developer, « Adopting SwiftData for a Core Data app », qui indique qu’avec la configuration par défaut « SwiftData copie le magasin existant dans le conteneur d’app group », tandis qu’une URL de magasin personnalisée vous laisse gérer l’emplacement. L’exigence d’entitlement CloudKit pour les membres de l’app group et la division en deux
ModelConfiguration(un synchronisé, un local) pour garder les widgets et les extensions hors du chemin de synchronisation ont été décrites lors du SwiftData Group Lab de la WWDC 2026. Paraphrasé à partir d’un enregistrement transcrit localement du SwiftData Group Lab de la WWDC 2026 ; Apple ne publie aucun sous-titre officiel pour les labs. ↩↩