Core Data vs SwiftData en Swift : implémentation technique détaillée
Core Data et SwiftData sont les deux grandes solutions d’Apple pour la persistance de données sur iOS / macOS. Core Data est l’outil historique, très puissant et très configurable. SwiftData est la nouvelle couche, construite pour Swift moderne et SwiftUI.
Cet article est volontairement plus technique que la version “non-technique” :
- on voit comment implémenter Core Data et SwiftData,
- on détaille les options possibles les plus courantes,
- on illustre avec des exemples de code concrets en Swift.
1. Core Data : architecture, stack et options
1.1 Rappels : à quoi sert Core Data ?
Core Data est un framework d’object graph management qui fournit :
- un modèle d’objets (entités, attributs, relations) ;
- un store (SQLite, in-memory, binaire) pour persister ces objets ;
- les outils pour créer / lire / mettre à jour / supprimer (CRUD) ;
- des options avancées : faulting, undo, batch updates, migration, contraintes…
1.2 Modèle de données Core Data (entités, attributs, relations)
Le modèle est généralement défini dans un fichier .xcdatamodeld via Xcode,
mais depuis quelques années, on peut aussi utiliser des classes Swift pures avec NSManagedObject.
Concepts principaux :
- Entity : un type d’objet (ex :
Note,User). - Attribute : un champ (ex :
title: String,createdAt: Date). - Relationship : lien entre entités (ex : un user a plusieurs notes).
Options importantes sur les attributs :
- Type : String, Integer, Boolean, Date, Binary, Transformable…
- Optional : autorise les valeurs nulles (
nil). - Default Value : valeur par défaut.
- Indexed : index pour accélérer certaines recherches.
- Transformable : pour stocker un type custom (codable, etc.).
Options importantes sur les relations :
- To-One / To-Many : relation 1–1, 1–N…
- Inverse : relation inverse (par ex. Note.user <–> User.notes).
- Delete Rule :
- Nullify : met l’inverse à
nilsans supprimer l’objet. (Exemple : On a un user : user1, on a deux notes : noteA et noteB, noteA.user = user1, noteB.user = user1, si on supprime user1, noteA.user et noteB.user deviennent nil) - Cascade : supprime les objets liés.
- Deny : empêche la suppression si des objets liés existent.
- No Action : ne fait rien (risque d’orphelins si mal utilisé).
- Nullify : met l’inverse à
1.3 Stack Core Data moderne avec NSPersistentContainer
La manière actuelle recommandée de configurer Core Data passe par NSPersistentContainer.
import CoreData
final class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "MyModel") // nom du .xcdatamodeld
if inMemory {
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]
} else {
// Options de migration légère
let description = container.persistentStoreDescriptions.first
description?.shouldMigrateStoreAutomatically = true
description?.shouldInferMappingModelAutomatically = true
}
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Erreur Core Data : \(error)")
}
}
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.automaticallyMergesChangesFromParent = true
}
var viewContext: NSManagedObjectContext {
container.viewContext
}
func newBackgroundContext() -> NSManagedObjectContext {
let context = container.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
return context
}
}
Options importantes :
NSInMemoryStoreTypevsNSSQLiteStoreType(par défaut) vs binaire (deprecated).shouldMigrateStoreAutomatically+shouldInferMappingModelAutomaticallypour la migration légère.mergePolicy: définit comment résoudre les conflits (store vs mémoire).automaticallyMergesChangesFromParent: fusionne automatiquement les changements venant des contextes enfants.
1.4 Exemple d’entité en code : Note
On va supposer une entité Note définie dans le modèle.
class Note: NSManagedObject {
@NSManaged var title: String
@NSManaged var content: String?
@NSManaged var createdAt: Date
}
Optionnel : on peut ajouter des computed properties non persistées (ex : un résumé du contenu).
1.5 CRUD avec Core Data
Création
func createNote(title: String, content: String?, context: NSManagedObjectContext) throws {
let note = Note(context: context)
note.title = title
note.content = content
note.createdAt = Date()
try context.save()
}
Lecture (fetch)
func fetchNotes(context: NSManagedObjectContext) throws -> [Note] {
let request: NSFetchRequest<Note> = Note.fetchRequest() as! NSFetchRequest<Note>
request.sortDescriptors = [
NSSortDescriptor(key: #keyPath(Note.createdAt), ascending: false)
]
// Options possibles :
// request.fetchLimit = 50
// request.fetchOffset = 0
// request.returnsObjectsAsFaults = true
// request.includesPendingChanges = true
return try context.fetch(request)
}
Mise à jour
On récupère une entité, on modifie ses propriétés, puis save().
func updateNote(_ note: Note, newTitle: String, context: NSManagedObjectContext) throws {
note.title = newTitle
try context.save()
}
Suppression
func deleteNote(_ note: Note, context: NSManagedObjectContext) throws {
context.delete(note)
try context.save()
}
1.6 Fetch avancé : prédicats, batch, faulting
Filtrer avec NSPredicate
// Notes dont le titre contient un mot
request.predicate = NSPredicate(format: "title CONTAINS[cd] %@", "swift")
Exemples d’opérateurs :
==,>,<,BETWEENCONTAINS,BEGINSWITH,ENDSWITHIN,ANY,ALL
Batch size, fetch limit
request.fetchBatchSize = 20 // charge par "pages"
request.fetchLimit = 100 // max 100 résultats
Faulting (optimisation mémoire)
Core Data peut ne pas charger immédiatement toutes les propriétés (faults). Cela améliore les perfs, surtout pour de gros jeux de données.
request.returnsObjectsAsFaults = true // par défaut true, on peut désactiver pour tout charger
1.7 Concurrence et contextes
Core Data n’est pas thread-safe par défaut. D’où les types de contextes :
.mainQueueConcurrencyType: pour l’UI..privateQueueConcurrencyType: pour le traitement en arrière-plan.
Avec NSPersistentContainer :
viewContext: main queue, pour l’UI.newBackgroundContext(): contexte en arrière-plan.
Pattern courant :
let backgroundContext = PersistenceController.shared.newBackgroundContext()
backgroundContext.perform {
do {
let notes = try fetchNotes(context: backgroundContext)
// Traitement lourd ici
// ...
try backgroundContext.save()
} catch {
print(error)
}
}
1.8 Batch updates / deletes
Pour modifier ou supprimer un grand nombre d’objets sans tout charger en mémoire :
Batch Update
let batch = NSBatchUpdateRequest(entityName: "Note")
batch.propertiesToUpdate = ["title": "Titre par défaut"]
batch.resultType = .updatedObjectsCountResultType
let result = try context.execute(batch) as? NSBatchUpdateResult
print("Mises à jour : \(result?.result ?? 0)")
Batch Delete
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Note.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
try context.execute(deleteRequest)
Attention : il faut généralement merge les changements dans le contexte pour que l’UI se mette à jour.
1.9 Migration et versioning
Quand on change le modèle (ajout d’attribut, renommage…), il faut gérer la migration.
Core Data propose :
- Migration légère :
- ajout d’attribut optionnel,
- renommage mappé,
- certains changements simples.
- Migration personnalisée :
- nécessite un mapping model et éventuellement un
NSEntityMigrationPolicy.
- nécessite un mapping model et éventuellement un
Les options vues plus haut (shouldMigrateStoreAutomatically et shouldInferMappingModelAutomatically) activent la migration légère automatique.
2. SwiftData : modèle, container, @Query, relations
2.1 Philosophie SwiftData
SwiftData est conçu pour :
- déclarer les modèles directement en Swift avec
@Model; - intégrer naturellement SwiftUI (
@Query,modelContext) ; - simplifier la configuration par rapport à Core Data.
2.2 Modèle SwiftData avec @Model
Exemple Note :
import SwiftData
@Model
class Note {
@Attribute(.unique) var id: UUID
var title: String
var content: String
var createdAt: Date
init(title: String, content: String) {
self.id = UUID()
self.title = title
self.content = content
self.createdAt = .now
}
}
Options possibles (principales) sur les propriétés :
@Attribute(.unique): contrainte d’unicité.@Attribute(.externalStorage): stockage externe pour les gros blobs.@Relationship(selon la version) : pour configurer les relations (inverse, deleteRule).@Transient(selon API) : pour des propriétés non persistées.
Les relations se déclarent avec des types Swift classiques :
@Model
class User {
var name: String
@Relationship(deleteRule: .cascade) var notes: [Note] = []
init(name: String) {
self.name = name
}
}
2.3 Configurer SwiftData dans l’app
On utilise généralement .modelContainer(for:) dans le point d’entrée SwiftUI.
import SwiftUI
import SwiftData
@main
struct NotesApp: App {
var body: some Scene {
WindowGroup {
NotesListView()
}
.modelContainer(for: [Note.self, User.self])
}
}
.modelContainer(for:) peut aussi recevoir une ModelConfiguration pour
définir :
- le type de store (par défaut, SQLite),
- le nom du fichier,
- des options de migration, etc. (API évolutive).
2.4 modelContext : le “contexte” SwiftData
SwiftData expose un ModelContext, concept similaire au NSManagedObjectContext de Core Data.
Dans SwiftUI :
struct NotesListView: View {
@Environment(\.modelContext) private var context
// ...
}
Avec ce contexte, on peut :
insertdes objets,delete,save(ou laisser SwiftData gérer selon configuration).
func addNote(title: String, content: String) {
let note = Note(title: title, content: content)
context.insert(note)
try? context.save()
}
2.5 @Query : requêtes réactives dans SwiftUI
@Query permet de lier une requête SwiftData à une vue SwiftUI.
Quand les données changent, la vue se met à jour automatiquement.
struct NotesListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \Note.createdAt, order: .reverse)
private var notes: [Note]
var body: some View {
List {
ForEach(notes) { note in
Text(note.title)
}
.onDelete(perform: delete)
}
.toolbar {
Button(action: addSampleNote) {
Image(systemName: "plus")
}
}
}
private func addSampleNote() {
let note = Note(title: "Nouvelle note", content: "Contenu")
context.insert(note)
try? context.save()
}
private func delete(at offsets: IndexSet) {
for index in offsets {
context.delete(notes[index])
}
try? context.save()
}
}
Options de @Query (principales) :
sort:: tri par une propriété (ou plusieurs).order::.forward/.reverse.- Filtrage possible via prédicats (selon version de l’API) avec
filter:ou closure.
2.6 Requêtes manuelles avec FetchDescriptor
En dehors de @Query, on peut utiliser FetchDescriptor pour des requêtes plus ciblées.
import SwiftData
func fetchNotesContaining(_ text: String, context: ModelContext) throws -> [Note] {
var descriptor = FetchDescriptor<Note>(
predicate: #Predicate { $0.title.contains(text) },
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
descriptor.fetchLimit = 50
return try context.fetch(descriptor)
}
Ici, #Predicate permet d’écrire des prédicats fortement typés en Swift.
2.7 Relations et delete rules en SwiftData
On peut configurer des relations à la manière de Core Data :
@Model
class User {
var name: String
@Relationship(deleteRule: .cascade, inverse: \Note.owner)
var notes: [Note] = []
init(name: String) {
self.name = name
}
}
@Model
class Note {
var title: String
var content: String
@Relationship(inverse: \User.notes)
var owner: User?
init(title: String, content: String, owner: User? = nil) {
self.title = title
self.content = content
self.owner = owner
}
}
Delete rules disponibles (parallèles à Core Data) : .cascade, .nullify, .deny, etc.
2.8 Migration et configuration SwiftData (vue d’ensemble)
SwiftData gère l’évolution du schéma via :
- l’analyse de vos modèles
@Model, - la configuration du
ModelContaineret desModelConfiguration.
De manière générale, l’idée est similaire à la migration légère de Core Data : les changements simples (ajout de propriété optionnelle, etc.) sont gérés automatiquement, pour les cas plus complexes, il faudra des stratégies plus avancées (et l’API évolue).
3. Core Data vs SwiftData : implémentation et choix
3.1 Quand privilégier Core Data ?
- App existante en production avec Core Data déjà en place.
- Support de versions iOS plus anciennes non compatibles SwiftData.
- Besoins avancés très précis déjà couverts par votre stack Core Data.
3.2 Quand privilégier SwiftData ?
- Nouvelle app en SwiftUI ciblant des OS récents (iOS 17+ selon le SDK).
- Envie de réduire la “plomberie” des modèles et du stack.
- Architecture moderne (MVI / MVVM + SwiftUI) avec logique réactive.
3.3 Coexistence ?
En pratique, on choisit généralement l’un ou l’autre pour un projet. On peut théoriquement faire cohabiter les deux, mais cela complique la maintenance (deux stacks de persistance à gérer et synchroniser).
4. Exemple de mise en place end-to-end (SwiftData dans un mini projet SwiftUI)
Pour finir, un exemple d’architecture simple complète avec SwiftData :
4.1 Modèle
@Model
class TaskItem {
var title: String
var isDone: Bool
var createdAt: Date
init(title: String, isDone: Bool = false) {
self.title = title
self.isDone = isDone
self.createdAt = .now
}
}
4.2 Point d’entrée de l’app
@main
struct TasksApp: App {
var body: some Scene {
WindowGroup {
TaskListView()
}
.modelContainer(for: TaskItem.self)
}
}
4.3 Vue principale
struct TaskListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \TaskItem.createdAt, order: .reverse)
private var tasks: [TaskItem]
@State private var newTitle: String = ""
var body: some View {
NavigationStack {
VStack {
HStack {
TextField("Nouvelle tâche", text: $newTitle)
Button("Ajouter") {
addTask()
}
.disabled(newTitle.isEmpty)
}
.padding()
List {
ForEach(tasks) { task in
HStack {
Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
.onTapGesture {
toggle(task)
}
Text(task.title)
}
}
.onDelete(perform: delete)
}
}
.navigationTitle("Mes tâches")
}
}
private func addTask() {
let item = TaskItem(title: newTitle)
context.insert(item)
try? context.save()
newTitle = ""
}
private func toggle(_ task: TaskItem) {
task.isDone.toggle()
try? context.save()
}
private func delete(at offsets: IndexSet) {
for index in offsets {
context.delete(tasks[index])
}
try? context.save()
}
}
En quelques dizaines de lignes, on a :
- un modèle persistant (
@Model), - un container de données (
.modelContainer), - des requêtes réactives (
@Query), - des opérations de persistance simples (
insert,delete,save).
Conclusion
Core Data et SwiftData offrent deux manières de gérer la persistance dans une app Swift :
- Core Data : très complet, mature, configurable, idéal pour les apps existantes et les besoins complexes ;
- SwiftData : moderne, concis, conçu pour SwiftUI, idéal pour les nouveaux projets ciblant des OS récents.
En comprenant :
- comment construire le modèle (entités, attributs, relations),
- comment configurer la stack (NSPersistentContainer vs ModelContainer),
- comment écrire des requêtes (NSFetchRequest/NSPredicate vs @Query/FetchDescriptor),
- et quelles options sont disponibles (delete rules, batch operations, @Attribute, @Relationship),
tu peux choisir et implémenter une solution de persistance adaptée à ton application, tout en gardant un code structuré, testable et évolutif.