Migration et chiffrement des bases Core Data et SwiftData : guide expert complet
La persistance locale est un aspect critique des applications iOS professionnelles. Au-delà du simple stockage, un produit en production doit gérer :
- l’évolution des modèles (migrations multi-versions),
- l’intégrité des données en production (compatibilité ascendante),
- la sécurité (chiffrement, protection physique et logique),
- la performance lors des mises à jour majeures.
Ce guide fournit une vision exhaustive, orientée production réelle : migrations avancées Core Data, migrations SwiftData via schémas versionnés, stratégie SQLCipher, et bonnes pratiques industrielles. Tout est illustré par des exemples concrets.
1. Migrations Core Data : de la légère au mapping model avancé
Core Data est l’outil le plus mature d'Apple pour gérer le stockage structuré. Il supporte plusieurs stratégies de migration, qui permettent de mettre à jour la structure d’une base existante sans perte de données.
1.1 Migration légère (lightweight migration)
La migration légère est la solution la plus courante. Core Data peut transférer les données vers le nouveau modèle lorsque les modifications sont compatibles :
- ajout d’un attribut optionnel,
- suppression d’un attribut optionnel,
- renommage d’un attribut avec Renaming Identifier,
- ajout/suppression de relations sans rupture.
Pour activer la migration légère :
let description = container.persistentStoreDescriptions.first
description?.shouldMigrateStoreAutomatically = true
description?.shouldInferMappingModelAutomatically = true
Exemple de renommage valide :
- Avant :
fullName - Après :
nameavec Renaming Identifier = fullName
1.2 Migration automatique avec inférence du mapping
Core Data peut “deviner” la transformation entre deux versions du modèle lorsque certains changements sont détectables :
- renommages avec identifiant,
- ajout d’attributs optionnels,
- évolution raisonnable du graphe relationnel.
Dans ce mode, Core Data génère lui-même un équivalent minimal de mapping model.
1.3 Migration manuelle : recours au .xcmappingmodel
Lorsque la migration est trop complexe, un mapping explicite est nécessaire.
Le mapping model est un fichier .xcmappingmodel généré par Xcode.
On l'utilise lorsque :
- une entité doit être divisée en plusieurs,
- deux entités doivent fusionner,
- une transformation métier est nécessaire (parser un champ, calculer une valeur),
- on change profondément la structure relationnelle.
1.4 Création d’un mapping model
Dans Xcode :
- File > New > File…
- Core Data > Mapping Model
- Choisir : Source Model (ex. ModelV1), Destination Model (ModelV2)
Chaque “Entity Mapping” décrit :
- l'entité source,
- l'entité destination,
- le type de mapping (Copy, Transform, Custom),
- les correspondances attribut par attribut,
- les relations à reconstruire.
1.5 Ajout d’une NSEntityMigrationPolicy
Pour les transformations avancées, on ajoute une classe qui personnalise la migration.
class UserMigrationPolicy: NSEntityMigrationPolicy {
override func createDestinationInstances(
forSource sInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
let dest = NSEntityDescription.insertNewObject(
forEntityName: mapping.destinationEntityName!,
into: manager.destinationContext
)
if let full = sInstance.value(forKey: "fullName") as? String {
let parts = full.split(separator: " ")
dest.setValue(parts.first.map(String.init), forKey: "firstName")
dest.setValue(parts.dropFirst().joined(separator: " "), forKey: "lastName")
}
manager.associate(
sourceInstance: sInstance,
withDestinationInstance: dest,
for: mapping
)
}
}
Cette classe doit être référencée dans le Mapping Model, au niveau de l’Entity Mapping.
1.6 Migration multi-étapes (V1 > V2 > V3 > …)
Pour les bases déjà en production depuis plusieurs années, une migration directe V1 > V5 est rarement fiable.
On privilégie la stratégie :
V1 > V2 > V3 > V4 > V5
Chaque étape possède soit :
- une migration légère,
- un mapping model précis.
1.7 Migration manuelle via NSMigrationManager
Cas extrêmes : migration bloquante, corruption, changements atypiques. On pilote la migration manuellement.
let migrationManager = NSMigrationManager(
sourceModel: sourceModel,
destinationModel: destModel
)
try migrationManager.migrateStore(
from: sourceURL,
sourceType: NSSQLiteStoreType,
options: nil,
with: mappingModel,
toDestinationURL: destURL,
destinationType: NSSQLiteStoreType,
destinationOptions: nil
)
2. Migrations SwiftData : VersionedSchema, plans de migration et transformations
SwiftData introduit un nouveau paradigme de migration, beaucoup plus structuré que la migration automatique de Core Data.
Le principe clé :
Chaque version de votre modèle SwiftData est exprimée via un “VersionedSchema”.
2.1 Déclarer les schémas versionnés
Exemple : Version 1 du modèle Note.
enum AppSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Note.self]
}
@Model
final class Note {
var title: String
var content: String
}
}
Version 2 du même modèle avec ajout d’un champ.
enum AppSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Note.self]
}
@Model
final class Note {
var title: String
var content: String
var createdAt: Date
}
}
2.2 Plan de migration (SchemaMigrationPlan)
Le plan décrit comment passer d’une version à la suivante.
enum AppMigrationPlan: SchemaMigrationPlan {
static var schemas: [VersionedSchema.Type] {
[
AppSchemaV1.self,
AppSchemaV2.self
]
}
static var stages: [MigrationStage] {
[
.lightweight(from: AppSchemaV1.self, to: AppSchemaV2.self)
]
}
}
2.3 Migration personnalisée
Exemple d’une transformation custom pour convertir un champ.
static var stages: [MigrationStage] {
[
.custom(
from: AppSchemaV1.self,
to: AppSchemaV2.self,
willMigrate: { context in
// Préparation éventuelle
},
didMigrate: { context in
// Exemple : initialiser createdAt manuellement
let descriptor = FetchDescriptor<AppSchemaV2.Note>()
let notes = try context.fetch(descriptor)
for note in notes {
note.createdAt = .now
}
try context.save()
}
)
]
}
2.4 Initialisation du container avec migration
let container = try ModelContainer(
for: AppSchemaV2.Note.self,
migrationPlan: AppMigrationPlan.self,
configurations: [
ModelConfiguration(schema: AppSchemaV2.self)
]
)
Ce mécanisme permet des migrations réelles multi-étapes comparable à Core Data mais plus modernes.
3. Chiffrement : protéger les bases Core Data et SwiftData
Le chiffrement dans iOS repose sur trois couches :
- le chiffrement matériel du disque,
- les protections de fichiers (
NSFileProtection), - un chiffrement applicatif ou SQLCipher.
3.1 Protection Apple : NSFileProtection
On peut définir une politique de protection pour le fichier SQLite :
let description = NSPersistentStoreDescription(url: storeURL)
description.setOption(
FileProtectionType.complete as NSObject,
forKey: NSPersistentStoreFileProtectionKey
)
Options principales :
.complete(le plus sécurisé),.completeUnlessOpen,.completeUntilFirstUserAuthentication.
3.2 SQLCipher + Core Data : chiffrement total de la base
Pour chiffrer réellement le contenu SQLite, on utilise SQLCipher :
let description = NSPersistentStoreDescription(url: storeURL)
description.type = NSSQLiteStoreType
description.setOption("PRAGMA key = 'monmotdepasse';" as NSString,
forKey: "sqlite pragmas")
SQLCipher fournit :
- chiffrement AES-256,
- pages SQLite chiffrées,
- résistance même à l’extraction physique du fichier.
3.3 SwiftData + chiffrement : état actuel
SwiftData n’offre pas encore d’API directe pour remplacer SQLite par SQLCipher : il utilise une infrastructure Core Data interne non exposée.
Approches réalistes aujourd'hui :
- NSFileProtection pour éviter les accès non autorisés hors device déverrouillé,
- chiffrement applicatif (ex. AES dans les champs sensibles),
- migration depuis SQLCipher via une lecture manuelle puis insertion dans SwiftData.
3.4 Exemple : migration SQLCipher > SwiftData
On ouvre l’ancienne base, on lit les lignes, on recrée les objets SwiftData.
func migrateFromSQLCipher(to context: ModelContext) throws {
let oldDB = try openEncryptedDB(path: "...", key: "secret")
for row in oldDB.fetchRows("SELECT title, content FROM notes") {
let note = Note(title: row["title"], content: row["content"])
context.insert(note)
}
try context.save()
}
3.5 Chiffrement applicatif (AES + Keychain)
Pour un contrôle total, on chiffre les données avant stockage :
let key = CryptoManager.shared.loadKeyFromKeychain()
let encryptedData = try AES.GCM.seal(
content.data(using: .utf8)!,
using: key
).combined
note.encryptedContent = encryptedData
Cette méthode garantit qu'aucune donnée sensible n’apparaît “en clair” dans la base SwiftData ou Core Data.
4. Stratégies professionnelles pour les migrations et la sécurité
4.1 Migrations Core Data : bonnes pratiques
- Une version > un modèle : éviter les suppressions d’anciennes versions.
- Préférer plusieurs petites migrations plutôt qu’une migration géante.
- Systématiser les “renaming identifiers”.
- Éviter les changements destructifs sans mapping model explicite.
4.2 Migrations SwiftData : bonnes pratiques
- Bien séparer chaque version avec
VersionedSchema. - Utiliser
SchemaMigrationPlanpour conserver la lisibilité. - Tester chaque étape avec une archive de données réelle.
- Préparer des migrations custom pour des transformations métier.
4.3 Sécurité & chiffrement : recommandations
- Utiliser
NSFileProtection.completepar défaut. - SQLCipher recommandé pour les applications réglementées (finance, santé).
- Chiffrement applicatif pour champs sensibles (identifiants, tokens, documents privés).
- Rotation de clés via Keychain Access.
Conclusion
Core Data et SwiftData offrent des mécanismes puissants de migration et de sécurité, mais leurs logiques diffèrent profondément :
- Core Data : système mature, migrations flexibles, compatible SQLCipher.
- SwiftData : versioning structuré, migrations déclaratives, API moderne.
Le chiffrement repose sur un ensemble complémentaire d’outils :
- protection système (NSFileProtection),
- chiffrement structurel (SQLCipher),
- chiffrement applicatif (AES + Keychain).
Avec ces techniques combinées, il est possible de créer des applications iOS robustes, sûres, capables d’évoluer sur plusieurs années avec une base de données maîtrisée, migrable et protégée.