SwiftData : le guide complet pour la persistance de données dans SwiftUI

Maîtrisez SwiftData pour SwiftUI : modèles @Model, requêtes @Query et #Predicate, opérations CRUD, relations, migrations VersionedSchema, synchronisation CloudKit et héritage de modèles iOS 26. Avec des exemples de code fonctionnels.

Introduction : pourquoi SwiftData change tout pour la persistance iOS

Si vous avez déjà travaillé avec Core Data, vous connaissez la chanson : un fichier .xcdatamodeld à maintenir, des sous-classes de NSManagedObject à générer, un stack de persistance à configurer manuellement, et des @FetchRequest parfois capricieux. Bref, ça fonctionne — mais avouons-le, c'est rarement agréable.

SwiftData, présenté par Apple à la WWDC 2023, change radicalement la donne. Ce framework de persistance, construit par-dessus Core Data, remplace tout le boilerplate par des macros Swift élégantes et une intégration native avec SwiftUI. Le résultat ? Définir un modèle de données, sauvegarder, interroger et synchroniser via iCloud demande une fraction du code qu'il fallait avant. Honnêtement, la première fois que j'ai migré un modèle Core Data vers SwiftData, j'ai été surpris de supprimer autant de lignes.

Avec iOS 26 (annoncé à la WWDC 2025), SwiftData franchit un nouveau cap grâce au support de l'héritage de classes et à la correction de bugs critiques rétrocompatibles jusqu'à iOS 17.

Dans ce guide, on couvre tout : de la création d'un modèle à la migration de schéma, en passant par les requêtes avancées, les relations et la synchronisation CloudKit. Avec des exemples de code fonctionnels à chaque étape. Allons-y.

Comprendre l'architecture de SwiftData

Avant de plonger dans le code, prenons un moment pour comprendre les trois piliers de SwiftData. L'architecture est simple (vraiment), mais la maîtriser vous évitera bien des maux de tête par la suite.

Le trio fondamental : ModelContainer, ModelContext et @Model

SwiftData s'articule autour de trois composants :

  • @Model : la macro qui transforme une classe Swift ordinaire en modèle persistant. Elle remplace NSManagedObject et le fichier .xcdatamodeld.
  • ModelContainer : le « backend » de persistance. Il crée et gère la base de données SQLite sous-jacente — c'est l'équivalent de NSPersistentContainer.
  • ModelContext : l'espace de travail en mémoire. Il suit les objets créés, modifiés et supprimés avant de les sauvegarder. Pensez-y comme l'équivalent de NSManagedObjectContext.

En pratique, votre app crée un ModelContainer au démarrage, qui fournit automatiquement un ModelContext principal (le « main context ») accessible dans toutes vos vues SwiftUI via l'environnement. Pas besoin de câblage supplémentaire.

Différence avec Core Data

Voici ce qui change concrètement :

  • Plus de fichier .xcdatamodeld — le schéma est défini directement dans votre code Swift
  • Plus de NSManagedObject — vos modèles sont de simples classes Swift annotées @Model
  • Sauvegarde automatique — SwiftData sauvegarde implicitement lors d'événements du cycle de vie de l'UI
  • Observable par défaut — les modèles se conforment automatiquement à Observable, Identifiable et Hashable

C'est un sacré gain de temps, surtout quand on démarre un nouveau projet.

Définir un modèle avec @Model

Commençons par le commencement : créer un modèle de données. La macro @Model est tout ce dont vous avez besoin :

import SwiftData

@Model
class Tache {
    var titre: String
    var estTerminee: Bool
    var dateCreation: Date
    var notes: String?

    init(titre: String, estTerminee: Bool = false, notes: String? = nil) {
        self.titre = titre
        self.estTerminee = estTerminee
        self.dateCreation = .now
        self.notes = notes
    }
}

C'est tout. Vraiment. Pas de fichier de modèle séparé, pas de génération de code. SwiftData déduit automatiquement le schéma de persistance à partir des propriétés de votre classe. Les types supportés incluent String, Int, Double, Bool, Date, Data, URL, ainsi que tout type Codable (structs, enums).

Personnaliser les propriétés avec @Attribute

La macro @Attribute permet d'ajouter des contraintes à vos propriétés :

@Model
class Utilisateur {
    @Attribute(.unique) var email: String
    var nom: String
    @Attribute(.externalStorage) var avatar: Data?
    @Attribute(.encrypt) var tokenSecret: String?

    init(email: String, nom: String) {
        self.email = email
        self.nom = nom
    }
}

Les options les plus utiles :

  • .unique — garantit l'unicité de la valeur (avec upsert automatique en cas de doublon, ce qui est plutôt malin)
  • .externalStorage — stocke les données volumineuses (images, fichiers) en dehors de la base SQLite
  • .encrypt — chiffre la propriété dans le store persistant
  • .spotlight — rend la propriété indexable par Spotlight

Propriétés transitoires

Si vous avez des propriétés calculées ou temporaires que vous ne voulez pas persister, utilisez @Transient :

@Model
class Article {
    var titre: String
    var contenu: String
    @Transient var nombreDeMots: Int {
        contenu.split(separator: " ").count
    }

    init(titre: String, contenu: String) {
        self.titre = titre
        self.contenu = contenu
    }
}

Configurer le ModelContainer dans votre app

Pour que SwiftData fonctionne, votre application doit disposer d'au moins un ModelContainer. La méthode la plus simple passe par le modifier .modelContainer sur votre WindowGroup.

Configuration basique

import SwiftUI
import SwiftData

@main
struct MonApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Tache.self)
    }
}

Cette seule ligne crée la base de données, configure le container et injecte un ModelContext principal dans l'environnement SwiftUI. Toutes les vues enfants y auront accès automatiquement — on est loin du boilerplate de Core Data.

Configuration avancée avec ModelConfiguration

Pour un contrôle plus fin (stockage en mémoire pour les tests, CloudKit, etc.), utilisez ModelConfiguration :

@main
struct MonApp: App {
    let container: ModelContainer

    init() {
        do {
            let config = ModelConfiguration(
                "MaBase",
                isStoredInMemoryOnly: false,
                allowsSave: true,
                cloudKitDatabase: .automatic
            )
            container = try ModelContainer(
                for: Tache.self, Utilisateur.self,
                configurations: config
            )
        } catch {
            fatalError("Impossible de créer le ModelContainer : \(error)")
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
}

Quelques options utiles de ModelConfiguration :

  • isStoredInMemoryOnly: true — parfait pour les previews et les tests unitaires
  • cloudKitDatabase: .automatic — active la synchronisation iCloud
  • allowsSave: false — mode lecture seule (pratique pour certains cas de debug)

Activer le undo/redo

SwiftData supporte nativement l'annulation et le rétablissement, et c'est aussi simple que ça :

.modelContainer(for: Tache.self, isUndoEnabled: true)

Opérations CRUD : créer, lire, modifier, supprimer

Toutes les opérations sur les données passent par le ModelContext. Dans une vue SwiftUI, vous y accédez via l'environnement :

@Environment(\.modelContext) private var context

Voyons chaque opération en détail.

Créer (Insert)

func ajouterTache(titre: String) {
    let nouvelleTache = Tache(titre: titre)
    context.insert(nouvelleTache)
    // Pas besoin d'appeler save() — SwiftData sauvegarde automatiquement
}

SwiftData utilise une sauvegarde implicite qui se déclenche lors d'événements du cycle de vie de l'UI (changement de scène, timer interne). Dans la majorité des cas, vous n'avez pas besoin d'appeler context.save() manuellement. C'est un vrai confort.

Lire (Query)

La macro @Query est le moyen le plus simple de récupérer des données dans une vue SwiftUI :

struct ListeTaches: View {
    @Query(sort: \Tache.dateCreation, order: .reverse)
    private var taches: [Tache]

    var body: some View {
        List(taches) { tache in
            HStack {
                Text(tache.titre)
                Spacer()
                if tache.estTerminee {
                    Image(systemName: "checkmark.circle.fill")
                        .foregroundStyle(.green)
                }
            }
        }
    }
}

@Query se met à jour automatiquement chaque fois que vos données changent, et votre vue SwiftUI se rafraîchit en conséquence. C'est la magie de l'intégration native — et franchement, ça change la vie par rapport aux @FetchRequest de Core Data.

Modifier (Update)

Modifier un objet SwiftData est d'une simplicité presque déconcertante. Vous modifiez directement les propriétés :

func basculerÉtat(tache: Tache) {
    tache.estTerminee.toggle()
    // C'est tout ! SwiftData détecte et sauvegarde le changement automatiquement
}

Pas de save(), pas de objectWillChange. SwiftData suit les modifications en interne grâce au protocole Observable et les sauvegarde lors du prochain cycle de persistance.

Supprimer (Delete)

func supprimer(tache: Tache) {
    context.delete(tache)
}

// Suppression depuis une liste avec swipe
func supprimerSélection(_ indexSet: IndexSet) {
    for index in indexSet {
        context.delete(taches[index])
    }
}

Requêtes avancées avec #Predicate et FetchDescriptor

La macro @Query est pratique pour les cas simples, mais quand les choses se corsent, SwiftData propose #Predicate et FetchDescriptor. Et c'est là que ça devient vraiment intéressant.

Filtrer avec #Predicate

#Predicate est le remplaçant moderne de NSPredicate. Il est typé, vérifié à la compilation et s'écrit en Swift natif — fini les chaînes de format obscures :

// Tâches non terminées, triées par date
@Query(
    filter: #Predicate { tache in
        tache.estTerminee == false
    },
    sort: \Tache.dateCreation,
    order: .reverse
)
private var tachesEnCours: [Tache]

Vous pouvez combiner des conditions :

@Query(
    filter: #Predicate { tache in
        !tache.estTerminee && tache.titre.contains("urgent")
    },
    sort: \Tache.dateCreation
)
private var tachesUrgentes: [Tache]

Attention : les comparaisons de chaînes comme contains() et starts(with:) sont sensibles à la casse. Et petit piège à connaître : les méthodes uppercased() et lowercased() ne sont pas supportées dans les prédicats.

FetchDescriptor pour un contrôle total

Pour les requêtes programmatiques (en dehors de @Query), utilisez FetchDescriptor avec le ModelContext :

func rechercherTaches(contenant texte: String) throws -> [Tache] {
    let prédicat = #Predicate { tache in
        tache.titre.contains(texte)
    }

    var descripteur = FetchDescriptor(
        predicate: prédicat,
        sortBy: [SortDescriptor(\Tache.dateCreation, order: .reverse)]
    )
    descripteur.fetchLimit = 20

    return try context.fetch(descripteur)
}

FetchDescriptor offre des options que @Query ne propose pas directement :

  • fetchLimit — limiter le nombre de résultats
  • fetchOffset — pagination
  • includePendingChanges — inclure ou exclure les changements non sauvegardés
  • relationshipKeyPathsForPrefetching — préchargement des relations (très utile pour éviter le N+1)

Requêtes dynamiques dans les vues

Un cas courant : filtrer les données en fonction d'un état de l'UI (barre de recherche, segment control). Le piège, c'est que @Query ne peut pas être modifié dynamiquement après l'initialisation de la vue.

La solution ? Injecter le filtre via l'initialiseur :

struct ListeTachesFiltree: View {
    @Query private var taches: [Tache]

    init(montrerTerminees: Bool) {
        let filtre = #Predicate { tache in
            montrerTerminees || !tache.estTerminee
        }
        _taches = Query(filter: filtre, sort: \Tache.dateCreation)
    }

    var body: some View {
        List(taches) { tache in
            Text(tache.titre)
        }
    }
}

Depuis la vue parente, vous passez simplement le paramètre :

struct ContentView: View {
    @State private var montrerTerminees = false

    var body: some View {
        ListeTachesFiltree(montrerTerminees: montrerTerminees)
    }
}

C'est un pattern que je me retrouve à utiliser constamment dans mes projets SwiftUI.

Gérer les relations entre modèles

Les applications réelles ne tournent jamais autour d'un seul modèle. SwiftData gère les relations entre modèles de manière plutôt élégante, que ce soit du one-to-many ou du many-to-many.

Relations one-to-many

Prenons l'exemple classique d'un projet contenant des tâches :

@Model
class Projet {
    var nom: String
    var dateCreation: Date
    @Relationship(deleteRule: .cascade, inverse: \Tache.projet)
    var taches: [Tache]

    init(nom: String) {
        self.nom = nom
        self.dateCreation = .now
        self.taches = []
    }
}

@Model
class Tache {
    var titre: String
    var estTerminee: Bool
    var projet: Projet?

    init(titre: String, projet: Projet? = nil) {
        self.titre = titre
        self.estTerminee = false
        self.projet = projet
    }
}

Points importants :

  • @Relationship(deleteRule: .cascade) — supprimer un projet supprime automatiquement toutes ses tâches
  • inverse: \Tache.projet — rend la relation bidirectionnelle explicite
  • Les relations sont chargées en lazy loading — SwiftData ne charge les tâches d'un projet que lorsque vous y accédez réellement

Les règles de suppression

SwiftData propose quatre règles de suppression (et choisir la bonne est plus important qu'on ne le pense) :

  • .nullify (par défaut) — met la référence inverse à nil
  • .cascade — supprime tous les objets liés en cascade
  • .deny — empêche la suppression si des objets liés existent
  • .noAction — ne fait rien (à utiliser avec prudence, voire pas du tout)

Relations many-to-many

Pour les relations many-to-many (par exemple des articles avec des étiquettes), les deux côtés utilisent des tableaux :

@Model
class ArticleBlog {
    var titre: String
    @Relationship(inverse: \Etiquette.articles)
    var etiquettes: [Etiquette]

    init(titre: String) {
        self.titre = titre
        self.etiquettes = []
    }
}

@Model
class Etiquette {
    var nom: String
    var articles: [ArticleBlog]

    init(nom: String) {
        self.nom = nom
        self.articles = []
    }
}

Important : SwiftData n'infère pas automatiquement les relations many-to-many. Vous devez les déclarer explicitement avec @Relationship. Si vous oubliez, la relation ne fonctionnera que dans un sens — et c'est le genre de bug qui peut vous faire perdre un après-midi entier.

Migrations de schéma avec VersionedSchema

Votre modèle de données va évoluer. C'est inévitable. La bonne nouvelle, c'est que SwiftData propose un système de migration assez robuste basé sur deux protocoles : VersionedSchema et SchemaMigrationPlan.

Migrations légères (automatiques)

SwiftData gère automatiquement les changements simples sans code supplémentaire :

  • Ajouter une propriété optionnelle ou avec valeur par défaut
  • Renommer une propriété (avec @Attribute(originalName:))
  • Supprimer une propriété
  • Modifier le type d'une relation

Pour ces cas-là, vous n'avez strictement rien à faire. SwiftData s'en occupe.

Migrations personnalisées

Pour les changements plus complexes, il faut définir des versions de schéma. Ça demande un peu plus de travail, mais ça reste bien plus lisible que les mappings Core Data :

// Version 1 : schéma initial
enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Tache.self]
    }

    @Model
    class Tache {
        var titre: String
        var estTerminee: Bool

        init(titre: String) {
            self.titre = titre
            self.estTerminee = false
        }
    }
}

// Version 2 : ajout d'une priorité
enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Tache.self]
    }

    @Model
    class Tache {
        var titre: String
        var estTerminee: Bool
        var priorite: Int

        init(titre: String, priorite: Int = 0) {
            self.titre = titre
            self.estTerminee = false
            self.priorite = priorite
        }
    }
}

Ensuite, définissez le plan de migration :

enum PlanMigrationTaches: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }

    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self
    ) { context in
        // Logique de migration personnalisée
        let taches = try context.fetch(FetchDescriptor())
        // Traitement des données existantes si nécessaire
    } didMigrate: { context in
        try context.save()
    }
}

Conseil important : définissez toujours votre schéma initial dans un VersionedSchema, même si vous n'avez pas encore de migration à effectuer. Croyez-moi, votre futur vous vous en remerciera quand il sera temps de faire évoluer le schéma.

Synchronisation iCloud avec CloudKit

L'un des avantages majeurs de SwiftData est sa synchronisation transparente avec iCloud. Et la configuration est étonnamment simple.

Prérequis

  1. Activer la capability iCloud dans votre target Xcode, avec CloudKit coché
  2. Ajouter un container CloudKit
  3. Activer la capability Background Modes et cocher « Remote Notifications »

Adapter vos modèles pour CloudKit

CloudKit impose des contraintes spécifiques sur vos modèles SwiftData — et c'est là que ça se complique un petit peu :

  • Toutes les propriétés doivent être optionnelles ou avoir une valeur par défaut
  • Toutes les relations doivent être optionnelles
  • Pas de contrainte .unique — CloudKit ne les supporte pas
// Modèle compatible CloudKit
@Model
class TacheCloud {
    var titre: String = ""
    var estTerminee: Bool = false
    var dateCreation: Date = .now
    var notes: String?
    var projet: ProjetCloud?

    init(titre: String) {
        self.titre = titre
    }
}

Ensuite, configurez le container avec CloudKit :

let config = ModelConfiguration(
    cloudKitDatabase: .automatic
)
let container = try ModelContainer(
    for: TacheCloud.self,
    configurations: config
)

Et voilà. SwiftData synchronisera automatiquement les données entre les appareils du même compte iCloud.

Limitation importante à garder en tête : SwiftData ne supporte actuellement que la synchronisation de données privées. Le partage de données entre différents Apple IDs (bases partagées ou publiques CloudKit) n'est pas encore pris en charge. Si vous avez besoin de cette fonctionnalité, Core Data reste malheureusement votre seule option.

Nouveauté iOS 26 : l'héritage de modèles

La grande nouveauté de SwiftData dans iOS 26, annoncée à la WWDC 2025, c'est le support de l'héritage de classes. Avant iOS 26, les modèles SwiftData ne pouvaient tout simplement pas hériter les uns des autres — une limitation qui frustrait pas mal de développeurs habitués aux hiérarchies de modèles Core Data.

Comment ça fonctionne

Vous pouvez maintenant définir une classe de base et créer des sous-classes qui héritent de ses propriétés :

@available(iOS 26, *)
@Model
class Voyage {
    var destination: String
    var dateDebut: Date
    var dateFin: Date

    init(destination: String, dateDebut: Date, dateFin: Date) {
        self.destination = destination
        self.dateDebut = dateDebut
        self.dateFin = dateFin
    }
}

@available(iOS 26, *)
@Model
class VoyageAffaires: Voyage {
    var entreprise: String
    var budget: Double

    init(destination: String, dateDebut: Date, dateFin: Date,
         entreprise: String, budget: Double) {
        self.entreprise = entreprise
        self.budget = budget
        super.init(destination: destination, dateDebut: dateDebut, dateFin: dateFin)
    }
}

@available(iOS 26, *)
@Model
class VoyagePersonnel: Voyage {
    var activites: [String]

    init(destination: String, dateDebut: Date, dateFin: Date,
         activites: [String] = []) {
        self.activites = activites
        super.init(destination: destination, dateDebut: dateDebut, dateFin: dateFin)
    }
}

Requêtes polymorphes avec « is »

iOS 26 permet aussi d'utiliser le mot-clé is dans les prédicats pour filtrer par type. C'est le genre de fonctionnalité qui manquait cruellement :

@available(iOS 26, *)
@Query(filter: #Predicate { voyage in
    voyage is VoyageAffaires
})
var voyagesAffaires: [Voyage]

Important : l'héritage de modèles est exclusivement disponible sur iOS 26+. Toute utilisation doit être annotée avec @available(iOS 26, *). Si votre cible de déploiement minimale est antérieure, vous ne pourrez pas en profiter pour le moment.

Corrections de bugs rétrocompatibles

Bonne nouvelle quand même : Xcode 26 a aussi corrigé des bugs critiques qui sont rétrocompatibles jusqu'à iOS 17 :

  • Les mises à jour de vues fonctionnent maintenant correctement lors de la mutation de données sous @ModelActor
  • Les propriétés de type Codable peuvent désormais être utilisées dans les prédicats (enfin !)

SwiftData vs Core Data : que choisir en 2026 ?

La question revient constamment : faut-il utiliser SwiftData ou Core Data ? La réponse dépend de votre contexte.

Choisissez SwiftData si :

  • Vous démarrez un nouveau projet ciblant iOS 17+
  • Votre application est de complexité simple à modérée
  • Vous développez avec SwiftUI (l'intégration est incomparable)
  • Vous privilégiez la rapidité de développement et la lisibilité du code
  • Vous n'avez besoin que de la synchronisation iCloud privée

Restez sur Core Data si :

  • Votre app doit supporter des appareils antérieurs à iOS 17
  • Vous avez besoin de fonctionnalités avancées : NSFetchedResultsController, opérations par lots, NSCompoundPredicate
  • Votre app nécessite le partage de données CloudKit entre différents utilisateurs
  • Vous maintenez un projet existant de grande envergure avec Core Data
  • Les performances sur de très gros volumes de données sont critiques

La coexistence est possible

Si vous migrez progressivement, sachez que SwiftData et Core Data peuvent coexister dans la même application, pointant vers le même fichier de base de données. La seule contrainte (et elle est de taille) : les deux schémas ne doivent pas diverger. Chaque modification apportée à l'un doit être reflétée dans l'autre.

Bonnes pratiques et pièges à éviter

Organisation du code

  • Un fichier par modèle — gardez vos classes @Model dans des fichiers séparés pour une maintenance plus facile
  • Utilisez des structs Codable pour les types complexes imbriqués plutôt que de créer des relations pour chaque sous-objet
  • Versionnez votre schéma dès le premier release — créez un VersionedSchema initial même si vous n'avez pas encore de migration

Performance

  • Utilisez fetchLimit — ne chargez pas tous les objets si vous n'en affichez que 20
  • Préchargez les relations avec relationshipKeyPathsForPrefetching si vous savez que vous en aurez besoin
  • Évitez les @Query trop larges — ils s'exécutent dès que la vue apparaît et peuvent bloquer l'UI sur de gros datasets

Pièges courants

  • Previews qui crashent — si vous créez une instance d'un modèle SwiftData sans ModelContainer autour, votre preview crashera. C'est le piège classique. Utilisez toujours un container en mémoire dans vos previews.
  • Thread safety — les ModelContainer peuvent être passés entre threads, mais les ModelContext doivent absolument rester sur le thread qui les a créés
  • Identifiants temporaires — après un insert(), l'objet reçoit un ID temporaire. Ne stockez pas cet ID ; ré-interrogez après save() si vous en avez besoin

Exemple complet : application de gestion de tâches

Pour conclure, voici un exemple fonctionnel qui combine tout ce qu'on a vu — modèles avec relations, CRUD, filtres dynamiques. Vous pouvez le copier-coller dans un projet Xcode et l'exécuter directement :

import SwiftUI
import SwiftData

// MARK: - Modèles

@Model
class Projet {
    var nom: String
    var couleur: String
    @Relationship(deleteRule: .cascade, inverse: \Tache.projet)
    var taches: [Tache]

    init(nom: String, couleur: String = "blue") {
        self.nom = nom
        self.couleur = couleur
        self.taches = []
    }
}

@Model
class Tache {
    var titre: String
    var estTerminee: Bool
    var dateCreation: Date
    var projet: Projet?

    init(titre: String, projet: Projet? = nil) {
        self.titre = titre
        self.estTerminee = false
        self.dateCreation = .now
        self.projet = projet
    }
}

// MARK: - Vue principale

struct ProjetDetailView: View {
    let projet: Projet
    @Environment(\.modelContext) private var context
    @State private var nouveauTitre = ""

    var body: some View {
        List {
            Section("Nouvelle tâche") {
                HStack {
                    TextField("Titre", text: $nouveauTitre)
                    Button("Ajouter") {
                        let tache = Tache(titre: nouveauTitre, projet: projet)
                        context.insert(tache)
                        nouveauTitre = ""
                    }
                    .disabled(nouveauTitre.isEmpty)
                }
            }

            Section("Tâches (\(projet.taches.count))") {
                ForEach(projet.taches.sorted(by: { $0.dateCreation > $1.dateCreation })) { tache in
                    HStack {
                        Button {
                            tache.estTerminee.toggle()
                        } label: {
                            Image(systemName: tache.estTerminee
                                  ? "checkmark.circle.fill"
                                  : "circle")
                            .foregroundStyle(tache.estTerminee ? .green : .gray)
                        }
                        Text(tache.titre)
                            .strikethrough(tache.estTerminee)
                    }
                }
                .onDelete { indexSet in
                    let sorted = projet.taches.sorted(by: { $0.dateCreation > $1.dateCreation })
                    for index in indexSet {
                        context.delete(sorted[index])
                    }
                }
            }
        }
        .navigationTitle(projet.nom)
    }
}

// MARK: - App

@main
struct GestionTachesApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationStack {
                ContentView()
            }
        }
        .modelContainer(for: [Projet.self, Tache.self])
    }
}

FAQ — Questions fréquentes sur SwiftData

SwiftData peut-il être utilisé avec Objective-C ?

Non. SwiftData repose exclusivement sur des fonctionnalités avancées de Swift (macros, types natifs, protocoles modernes) qui n'ont pas d'équivalent en Objective-C. Si votre projet contient du code Objective-C, vous pouvez utiliser SwiftData uniquement dans les parties Swift.

Peut-on utiliser SwiftData sans SwiftUI ?

Oui, mais avec des limitations. La macro @Query ne fonctionne que dans les vues SwiftUI, mais vous pouvez utiliser FetchDescriptor et ModelContext directement dans n'importe quel code Swift — y compris UIKit. Par contre, vous perdez la réactivité automatique des vues et devrez gérer les mises à jour manuellement.

Quelle est la version minimale d'iOS requise pour SwiftData ?

SwiftData nécessite iOS 17 (ou macOS 14, watchOS 10, tvOS 17) au minimum. L'héritage de modèles nécessite iOS 26. Si vous devez supporter des versions antérieures, Core Data reste votre seule option pour la persistance native Apple.

Comment gérer les previews Xcode avec SwiftData ?

Les previews crashent si aucun ModelContainer n'est disponible. La solution classique est d'utiliser un container en mémoire :

#Preview {
    let config = ModelConfiguration(isStoredInMemoryOnly: true)
    let container = try! ModelContainer(for: Tache.self, configurations: config)
    return ListeTaches()
        .modelContainer(container)
}

SwiftData est-il adapté aux applications en production ?

Oui, à condition de cibler iOS 17+. SwiftData est utilisé en production par de nombreuses applications depuis 2024. Cela dit, gardez en tête ses limitations actuelles : pas de synchronisation CloudKit partagée, pas de NSFetchedResultsController, et des performances légèrement inférieures à Core Data sur de très gros volumes. Pour la majorité des apps de complexité simple à modérée, SwiftData est tout à fait prêt pour la production.

À propos de l'auteur Editorial Team

Our team of expert writers and editors.