SwiftData
Module 11 — Notes de cours · Persistance moderne avec SwiftData
iOS 17+ · Xcode 15+ · Swift 5.9+01 Introduction
SwiftData est le framework de persistance de données moderne d'Apple, introduit à la WWDC 2023 avec iOS 17. Il a été conçu pour remplacer progressivement Core Data en offrant une approche déclarative totalement intégrée à SwiftUI.
Là où Core Data demande de configurer un fichier .xcdatamodeld, de gérer
manuellement un NSPersistentContainer, et d'apprendre une API ancienne, SwiftData
permet de définir ses modèles directement dans le code Swift, avec des macros.
@Published,
de ObservableObject, ni de @FetchRequest. Un seul macro @Query
suffit.02 SwiftData vs Core Data
Voici un aperçu des équivalences entre les deux frameworks :
| Concept | Core Data | SwiftData |
|---|---|---|
| Définir un modèle | .xcdatamodeld (éditeur visuel) | @Model (classe Swift pure) |
| Initialiser le stockage | NSPersistentContainer | ModelContainer |
| Zone de travail en mémoire | NSManagedObjectContext | ModelContext |
| Lire des données dans SwiftUI | @FetchRequest | @Query |
| Filtrer des données | NSPredicate (String non vérifiée) | #Predicate (vérifié à la compilation) |
| Trier des données | NSSortDescriptor | SortDescriptor + FetchDescriptor |
| Relations | Via l'éditeur visuel | @Relationship (dans le code) |
| Injecter dans SwiftUI | .environment(\.managedObjectContext, ...) |
.modelContainer(for:) |
| Minimum iOS | iOS 3+ | iOS 17+ |
03 Composants principaux
La Stack SwiftData est plus simple que la Stack Core Data — elle se résume à trois composants que vous utilisez directement :
Stack SwiftData
Comparé à Core Data, le Persistent Store Coordinator et le Persistent Store sont complètement cachés par SwiftData — vous n'en avez jamais besoin directement.
Transforme une classe Swift ordinaire en modèle persistant. Remplace le fichier .xcdatamodeld.
Configure la base de données. Se déclare une seule fois dans le point d'entrée de l'app avec .modelContainer(for:).
Permet d'insérer, de modifier et de supprimer des objets. Récupéré via @Environment(\.modelContext).
Récupère et observe automatiquement les données. La vue se met à jour seule à chaque changement.
04 @Model — Définir un modèle
Le macro @Model est le cœur de SwiftData. On l'applique à une classe Swift pour
la transformer en modèle persistant. SwiftData génère automatiquement tout le code nécessaire
pour stocker et retrouver les instances de cette classe.
@Model, c'est comme remplir une fiche de formulaire
pour décrire vos données. SwiftData lit cette fiche et crée automatiquement la table correspondante
dans SQLite — sans que vous ayez à écrire une ligne de SQL.import SwiftData // @Model transforme cette classe en modèle persistant @Model final class Product { var name: String var quantity: Int // Utilisez Int, pas String, pour les nombres var price: Double var addedAt: Date init(name: String, quantity: Int, price: Double) { self.name = name self.quantity = quantity self.price = price self.addedAt = Date.now } }
final est recommandé pour la performance.@Model directement.@Attribute — Contraintes sur les propriétés
Le macro @Attribute permet d'ajouter des options spéciales à une propriété
du modèle — par exemple, forcer l'unicité ou externaliser de gros fichiers.
@Model final class Product { // Valeur unique — SwiftData refuse les doublons sur ce champ @Attribute(.unique) var name: String // Stocké en dehors du fichier SQLite (recommandé pour les images > quelques Ko) @Attribute(.externalStorage) var thumbnail: Data? var quantity: Int var price: Double init(name: String, quantity: Int, price: Double) { self.name = name self.quantity = quantity self.price = price } }
@Transient — Propriétés non persistées
Par défaut, toutes les propriétés d'un modèle sont persistées. Si vous avez besoin
d'une propriété temporaire (calculée, de débogage, etc.) qui ne doit pas être
sauvegardée dans la base, utilisez @Transient.
@Model final class Product { var name: String var quantity: Int var price: Double // Cette propriété existe seulement en mémoire — jamais sauvegardée @Transient var isBeingEdited: Bool = false init(name: String, quantity: Int, price: Double) { self.name = name self.quantity = quantity self.price = price } }
05 ModelContainer — Configurer le stockage
Le ModelContainer est le pont entre vos modèles et la base de données SQLite.
On le configure une seule fois, à la racine de l'application, et SwiftUI
l'injecte automatiquement dans tout l'arbre de vues.
import SwiftUI import SwiftData @main struct CoreDataDemoApp: App { var body: some Scene { WindowGroup { ContentView() } // Une seule ligne suffit pour configurer toute la persistance .modelContainer(for: [Product.self]) } } // Pour plusieurs modèles : .modelContainer(for: [Product.self, Category.self, Order.self])
ModelConfiguration — Options avancées
Si vous avez besoin de personnaliser le comportement du conteneur (stockage en mémoire,
lecture seule, emplacement personnalisé), utilisez ModelConfiguration.
// Stockage en mémoire (données perdues à la fermeture — utile pour les previews) let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try ModelContainer(for: Product.self, configurations: config) // Emplacement personnalisé sur disque let url = URL.applicationSupportDirectory.appending(path: "myapp.store") let config = ModelConfiguration(url: url) // Lecture seule (utile pour des données pré-chargées) let config = ModelConfiguration(isReadOnly: true)
isStoredInMemoryOnly: true dans vos
#Preview pour éviter de polluer la vraie base de données lors du développement.06 ModelContext — Travailler avec les données
Le ModelContext est votre espace de travail au quotidien. Toutes les opérations
sur les données (créer, modifier, supprimer, sauvegarder) passent par lui.
Dans SwiftUI, on récupère le contexte depuis l'environnement :
import SwiftUI import SwiftData struct ContentView: View { // ModelContext fourni automatiquement par le .modelContainer de l'App @Environment(\.modelContext) private var context var body: some View { ... } }
Opérations CRUD
context.insert(obj)context.delete(obj)try context.save()// Créer et insérer let product = Product(name: "Pomme", quantity: 12, price: 1.99) context.insert(product) try? context.save() // Modifier // Il suffit de modifier la propriété — @Model suit les changements automatiquement product.quantity = 20 try? context.save() // Supprimer context.delete(product) try? context.save() // Supprimer depuis une liste (avec .onDelete) private func deleteProducts(at offsets: IndexSet) { for index in offsets { context.delete(products[index]) } try? context.save() }
insert.
SwiftData suit les changements automatiquement. Il suffit d'appeler save().Autosave — Sauvegarde automatique
Par défaut, SwiftData active l'autosave : le contexte se sauvegarde
automatiquement lorsque l'application passe en arrière-plan ou lors d'événements système.
Dans la plupart des cas, vous n'avez donc pas besoin d'appeler save() manuellement.
.modelContainer(for:isAutosaveEnabled: false). Utile si vous gérez des transactions complexes ou des annulations.ModelContext(container)) n'ont pas d'autosave — il faut appeler save() explicitement.07 @Query — Lire les données dans SwiftUI
@Query est le macro SwiftUI de SwiftData pour lire des données depuis la base.
Il remplace @FetchRequest de Core Data. La vue se met à jour automatiquement
dès qu'un changement est détecté.
@Query, c'est comme un fil RSS en direct.
Vous vous abonnez une fois, et chaque fois que quelque chose change dans la base,
votre vue reçoit la mise à jour automatiquement — sans action de votre part.struct ContentView: View { // Récupère tous les produits, sans tri @Query var products: [Product] // Trié alphabétiquement par nom (A → Z) @Query(sort: \.name) var products: [Product] // Trié par date d'ajout, ordre décroissant (plus récent en premier) @Query(sort: \.addedAt, order: .reverse) var products: [Product] // Trié par plusieurs critères @Query(sort: [ SortDescriptor(\.name), SortDescriptor(\.addedAt, order: .reverse) ]) var products: [Product] var body: some View { List(products) { product in Text(product.name) } } }
.forward).@Query utilise un tableau
Swift natif ([Product]) plutôt que FetchedResults<Product>.
C'est plus simple à manipuler et compatible avec toutes les APIs Swift standard.08 #Predicate & FetchDescriptor — Filtrer
Pour filtrer les résultats, SwiftData utilise #Predicate — une version moderne
et type-safe de NSPredicate. La syntaxe est du Swift pur,
vérifiée à la compilation grâce aux macros.
// Produits dont le nom contient "pomme" (insensible à la casse) let searchText = "pomme" let pred = #Predicate<Product> { $0.name.localizedStandardContains(searchText) } // Produits dont la quantité est supérieure à 10 let pred = #Predicate<Product> { $0.quantity > 10 } // Combinaison de critères let minQty = 5 let pred = #Predicate<Product> { $0.name.localizedStandardContains(searchText) && $0.quantity > minQty }
NSPredicate qui accepte des chaînes
comme "name CONTAINS[cd] %@", #Predicate est vérifié à la compilation.
Si vous faites une faute de frappe ou changez le nom d'une propriété, Xcode vous le signale
immédiatement — impossible d'avoir un crash en production pour une faute dans une string.Utiliser #Predicate avec @Query
struct ContentView: View { @State private var searchText = "" var body: some View { FilteredProductsView(search: searchText) } } // @Query ne peut pas utiliser des variables de la vue directement. // On crée une sous-vue qui reçoit la valeur et initialise @Query dans son init. struct FilteredProductsView: View { @Query private var products: [Product] init(search: String) { let predicate = #Predicate<Product> { search.isEmpty || $0.name.localizedStandardContains(search) } _products = Query(filter: predicate, sort: \.name) } var body: some View { List(products) { product in Text(product.name) } } }
@Query ne peut pas capturer directement
des variables @State de la vue parente. La solution standard est de créer une
sous-vue qui reçoit la valeur et initialise @Query dans son init(),
comme montré ci-dessus.Requête ponctuelle avec FetchDescriptor
Pour les cas où vous ne voulez pas de mise à jour automatique (par exemple dans une fonction
de recherche ou un traitement en arrière-plan), utilisez FetchDescriptor avec
context.fetch().
let predicate = #Predicate<Product> { $0.quantity > 0 } var descriptor = FetchDescriptor<Product>( predicate: predicate, sortBy: [SortDescriptor(\.name)] ) descriptor.fetchLimit = 20 // Limiter le nombre de résultats let results = try? context.fetch(descriptor)
09 @Relationship — Relations entre modèles
Le macro @Relationship définit des liens entre deux modèles. SwiftData gère
automatiquement la cohérence des données dans les deux sens.
@Model final class Category { var name: String // Une catégorie contient plusieurs produits (one-to-many) // deleteRule: .cascade → supprimer la catégorie supprime aussi ses produits @Relationship(deleteRule: .cascade) var products: [Product] = [] init(name: String) { self.name = name } } @Model final class Product { var name: String var quantity: Int // Relation inverse — un produit appartient à une catégorie var category: Category? init(name: String, quantity: Int) { self.name = name self.quantity = quantity } } // Utilisation : let cat = Category(name: "Fruits") let p = Product(name: "Pomme", quantity: 10) cat.products.append(p) context.insert(cat) try? context.save()
La règle de suppression (deleteRule) détermine ce qui arrive aux objets liés
quand l'objet parent est supprimé :
| Règle | Comportement | Cas d'usage typique |
|---|---|---|
| .cascade | Supprime tous les objets liés automatiquement | Commandes liées à un client — si le client est supprimé, les commandes aussi |
| .nullify | Met la référence à nil dans les objets liés |
Produits liés à une catégorie — si la catégorie est supprimée, les produits restent mais sans catégorie |
| .deny | Empêche la suppression si des objets liés existent encore | Compte bancaire avec des transactions — impossible de supprimer le compte tant qu'il y a des transactions |
| .noAction | Ne fait rien — les références restent en place | Rarement utilisé. À éviter si possible (peut laisser des références orphelines). |
Passer au laboratoire
Créer une application SwiftData de A à Z avec SwiftUI