← Accueil IFM025921 · Module 11 · Notes de cours Requiert iOS 17+

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.

Conçu pour SwiftUI : SwiftData s'intègre nativement avec SwiftUI. Les données se synchronisent automatiquement avec l'interface — pas besoin de @Published, de ObservableObject, ni de @FetchRequest. Un seul macro @Query suffit.
Sous le capot : SwiftData est bâti par-dessus Core Data. Il utilise le même moteur SQLite, mais expose une API Swift moderne. Les deux frameworks peuvent coexister dans le même projet lors d'une migration progressive.

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

Application SwiftUIVotre code — vues, @Query
ModelContextZone de travail (créer, modifier, supprimer)
ModelContainerPont entre le contexte et le stockage
Base de données SQLiteFichier sur disque — géré automatiquement

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.

@Model
Définir la structure

Transforme une classe Swift ordinaire en modèle persistant. Remplace le fichier .xcdatamodeld.

ModelContainer
Gérer le stockage

Configure la base de données. Se déclare une seule fois dans le point d'entrée de l'app avec .modelContainer(for:).

ModelContext
Zone de travail

Permet d'insérer, de modifier et de supprimer des objets. Récupéré via @Environment(\.modelContext).

@Query
Lire les données

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.

Analogie : @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.
Product.swift
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
    }
}
@Model
Macro Swift qui rend la classe observable et persistante en une seule ligne. Génère automatiquement les méthodes de suivi des changements.
final class
SwiftData requiert des classes (pas des structs), car il doit suivre les changements par référence. final est recommandé pour la performance.
Propriétés Swift
Toutes les propriétés dont le type est supporté (String, Int, Double, Bool, Date, URL, UUID…) sont automatiquement persistées — aucune annotation nécessaire.
Pas besoin de fichier .xcdatamodeld : Toute la définition du schéma se fait dans le code Swift. Xcode comprend la structure à partir du macro @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.

Product.swift
@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
    }
}
@Attribute(.unique)
SwiftData refuse l'insertion si une valeur identique existe déjà. Équivalent d'une contrainte UNIQUE en SQL.
@Attribute(.externalStorage)
Les données volumineuses (images, PDF) sont stockées dans un fichier séparé plutôt qu'inline dans SQLite — améliore les performances.

@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.

Product.swift
@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.

Analogie : Le ModelContainer, c'est comme l'entrepôt d'une entreprise. Vous le configurez une fois (où il est, sa taille, ses règles). Ensuite les employés (ModelContext) vont chercher et ranger des objets dedans au quotidien.
CoreDataDemoApp.swift
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.

Exemples 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)
Previews Xcode : Utilisez 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.

Analogie : Le ModelContext, c'est comme un brouillon dans un traitement de texte. Vous pouvez écrire, effacer, modifier librement — rien n'est enregistré sur disque tant que vous n'avez pas cliqué Sauvegarder. Si vous fermez sans sauvegarder, vos changements sont perdus.

Dans SwiftUI, on récupère le contexte depuis l'environnement :

ContentView.swift
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

Insert
context.insert(obj)
Update
Modifier les propriétés directement
Delete
context.delete(obj)
Save
try context.save()
Opérations CRUD complètes
//  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()
}
Modifier sans insert : Si vous modifiez une propriété d'un objet déjà persisté, vous n'avez pas besoin de rappeler 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.

Autosave ON (défaut)
Le système sauvegarde périodiquement et lors du passage en arrière-plan. Recommandé pour la majorité des applications.
Autosave OFF
Désactivé via .modelContainer(for:isAutosaveEnabled: false). Utile si vous gérez des transactions complexes ou des annulations.
Contextes manuels
Les contextes créés manuellement (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é.

Analogie : @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.
ContentView.swift
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)
        }
    }
}
@Query var products: [Product]
Lit tous les objets Product depuis la base. Se met à jour automatiquement quand un produit est ajouté, modifié ou supprimé.
sort: \.name
Tri par keypath Swift — vérifié à la compilation. Bien plus sûr que les strings de Core Data.
order: .reverse
Inverse l'ordre de tri. Par défaut l'ordre est croissant (.forward).
@Query vs Core Data @FetchRequest : @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.

Exemples #Predicate
// 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
}
Type-safe : Contrairement à 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

ContentView.swift
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)
        }
    }
}
Limitation @Query : @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().

Requête ponctuelle
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.

Exemple — Category + Product
@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