-
SwiftData : En savoir plus sur l’héritage et la migration des schémas
Découvrez comment utiliser l'héritage de classes pour modéliser vos données. Apprenez à optimiser les requêtes et à migrer de manière transparente les données de votre app pour tirer parti de l'héritage. Explorez l'utilisation des sous-classes pour créer des graphes de modèles, concevoir des extractions et des requêtes efficaces, et implémenter des migrations de schéma robustes. Découvrez comment suivre efficacement les modifications grâce à Observable et à l'historique persistant.
Chapitres
- 0:00 - Introduction
- 2:11 - Maîtriser l’héritage de classe
- 7:39 - Faire évoluer les données avec la migration
- 11:27 - Personnalisation des données récupérées
- 13:54 - Observation des changements apportés aux données
- 18:28 - Étapes suivantes
Ressources
Vidéos connexes
WWDC25
WWDC24
-
Rechercher dans cette vidéo…
-
-
1:07 - Import SwiftData and add @Model
// Trip Models decorated with @Model import Foundation import SwiftData @Model class Trip { var name: String var destination: String var startDate: Date var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? } @Model class BucketListItem { ... } @Model class LivingAccommodation { ... } -
1:18 - Add modelContainer modifier
// SampleTrip App using modelContainer Scene modifier import SwiftUI import SwiftData @main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self) } } -
1:30 - Adopt @Query
// Trip App using @Query import SwiftUI import SwiftData struct ContentView: View { @Query var trips: [Trip] var body: some View { NavigationSplitView { List(selection: $selection) { ForEach(trips) { trip in TripListItem(trip: trip) } } } } } -
3:28 - Add subclasses to Trip
// Trip Model extended with two new subclasses @Model class Trip { var name: String var destination: String var startDate: Date var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? } @available(iOS 26, *) @Model class BusinessTrip: Trip { var perdiem: Double = 0.0 } @available(iOS 26, *) @Model class PersonalTrip: Trip { enum Reason: String, CaseIterable, Codable { case family case reunion case wellness } var reason: Reason } -
4:03 - Update modelContainer modifier
// SampleTrip App using modelContainer Scene modifier import SwiftUI import SwiftData @main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: [Trip.self, BusinessTrip.self, PersonalTrip.self]) } } -
7:06 - Add segmented control to drive a predicate to filter by Type
// Trip App add segmented control import SwiftUI import SwiftData struct ContentView: View { @Query var trips: [Trip] enum Segment: String, CaseIterable { case all = "All" case personal = "Personal" case business = "Business" } init() { let classPredicate: Predicate<Trip>? = { switch segment.wrappedValue { case .personal: return #Predicate { $0 is PersonalTrip } case .business: return #Predicate { $0 is BusinessTrip } default: return nil } } _trips = Query(filter: classPredicate, sort: \.startDate, order: .forward) } var body: some View { ... } } -
8:26 - SampleTrips Versioned Schema 2.0
enum SampleTripsSchemaV2: VersionedSchema { static var versionIdentifier: Schema.Version { Schema.Version(2, 0, 0) } static var models: [any PersistentModel.Type] { [SampleTripsSchemaV2.Trip.self, BucketListItem.self, LivingAccommodation.self] } @Model class Trip { @Attribute(.unique) var name: String var destination: String @Attribute(originalName: "start_date") var startDate: Date @Attribute(originalName: "end_date") var endDate: Date var bucketList: [BucketListItem]? = [] var livingAccommodation: LivingAccommodation? ... } } -
8:41 - SampleTrips Custom Migration Stage from Version 1.0 to 2.0
static let migrateV1toV2 = MigrationStage.custom( fromVersion: SampleTripsSchemaV1.self, toVersion: SampleTripsSchemaV2.self, willMigrate: { context in let fetchDesc = FetchDescriptor<SampleTripsSchemaV1.Trip>() let trips = try? context.fetch(fetchDesc) // De-duplicate Trip instances here... try? context.save() }, didMigrate: nil ) -
9:09 - SampleTrips Versioned Schema 3.0
enum SampleTripsSchemaV3: VersionedSchema { static var versionIdentifier: Schema.Version { Schema.Version(3, 0, 0) } static var models: [any PersistentModel.Type] { [SampleTripsSchemaV3.Trip.self, BucketListItem.self, LivingAccommodation.self] } @Model class Trip { #Unique<Trip>([\.name, \.startDate, \.endDate]) #Index<Trip>([\.name], [\.startDate], [\.endDate], [\.name, \.startDate, \.endDate]) @Attribute(.preserveValueOnDeletion) var name: String @Attribute(hashModifier:@"v3") var destination: String @Attribute(.preserveValueOnDeletion, originalName: "start_date") var startDate: Date @Attribute(.preserveValueOnDeletion, originalName: "end_date") var endDate: Date } } -
9:33 - SampleTrips Custom Migration Stage from Version 2.0 to 3.0
static let migrateV2toV3 = MigrationStage.custom( fromVersion: SampleTripsSchemaV2.self, toVersion: SampleTripsSchemaV3.self, willMigrate: { context in let trips = try? context.fetch(FetchDescriptor<SampleTripsSchemaV2.Trip>()) // De-duplicate Trip instances here... try? context.save() }, didMigrate: nil ) -
9:50 - SampleTrips Versioned Schema 4.0
@available(iOS 26, *) enum SampleTripsSchemaV4: VersionedSchema { static var versionIdentifier: Schema.Version { Schema.Version(4, 0, 0) } static var models: [any PersistentModel.Type] { [Trip.self, BusinessTrip.self, PersonalTrip.self, BucketListItem.self, LivingAccommodation.self] } } -
10:03 - SampleTrips Lightweight Migration Stage from Version 3.0 to 4.0
@available(iOS 26, *) static let migrateV3toV4 = MigrationStage.lightweight( fromVersion: SampleTripsSchemaV3.self, toVersion: SampleTripsSchemaV4.self ) -
10:24 - SampleTrips Schema Migration Plan
enum SampleTripsMigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { var currentSchemas: [any VersionedSchema.Type] = [SampleTripsSchemaV1.self, SampleTripsSchemaV2.self, SampleTripsSchemaV3.self] if #available(iOS 26, *) { currentSchemas.append(SampleTripsSchemaV4.self) } return currentSchemas } static var stages: [MigrationStage] { var currentStages = [migrateV1toV2, migrateV2toV3] if #available(iOS 26, *) { currentStages.append(migrateV3toV4) } return currentStages } } -
10:51 - Use Schema Migration Plan with ModelContainer
// SampleTrip App update modelContainer Scene modifier for migrated container @main struct TripsApp: App { let container: ModelContainer = { do { let schema = Schema(versionedSchema: SampleTripsSchemaV4.self) container = try ModelContainer( for: schema, migrationPlan: SampleTripsMigrationPlan.self) } catch { ... } return container }() var body: some Scene { WindowGroup { ContentView() } .modelContainer(container) } } -
11:48 - Add search predicate to Query
// Trip App add search text to predicate struct ContentView: View { @Query var trips: [Trip] init( ... ) { let classPredicate: Predicate<Trip>? = { switch segment.wrappedValue { case .personal: return #Predicate { $0 is PersonalTrip } case .business: return #Predicate { $0 is BusinessTrip } default: return nil } } let searchPredicate = #Predicate<Trip> { searchText.isEmpty ? true : $0.name.localizedStandardContains(searchText) || $0.destination.localizedStandardContains(searchText) } let fullPredicate: Predicate<Trip> if let classPredicate { fullPredicate = #Predicate { classPredicate.evaluate($0) && searchPredicate.evaluate($0)} } else { fullPredicate = searchPredicate } _trips = Query(filter: fullPredicate, sort: \.startDate, order: .forward) } var body: some View { ... } } -
12:31 - Tailor SwiftData Fetch in Custom Migration Stage
static let migrateV1toV2 = MigrationStage.custom( fromVersion: SampleTripsSchemaV1.self, toVersion: SampleTripsSchemaV2.self, willMigrate: { context in var fetchDesc = FetchDescriptor<SampleTripsSchemaV1.Trip>() fetchDesc.propertiesToFetch = [\.name] let trips = try? context.fetch(fetchDesc) // De-duplicate Trip instances here... try? context.save() }, didMigrate: nil ) -
13:11 - Add relationshipsToPrefetch in Custom Migration Stage
static let migrateV1toV2 = MigrationStage.custom( fromVersion: SampleTripsSchemaV1.self, toVersion: SampleTripsSchemaV2.self, willMigrate: { context in var fetchDesc = FetchDescriptor<SampleTripsSchemaV1.Trip>() fetchDesc.propertiesToFetch = [\.name] fetchDesc.relationshipKeyPathsForPrefetching = [\.livingAccommodation] let trips = try? context.fetch(fetchDesc) // De-duplicate Trip instances here... try? context.save() }, didMigrate: nil ) -
13:28 - Update Widget to harness fetchLimit
// Widget code to get new Timeline Entry func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) { let currentDate = Date.now var fetchDesc = FetchDescriptor(sortBy: [SortDescriptor(\Trip.startDate, order: .forward)]) fetchDesc.predicate = #Predicate { $0.endDate >= currentDate } fetchDesc.fetchLimit = 1 let modelContext = ModelContext(DataModel.shared.modelContainer) if let upcomingTrips = try? modelContext.fetch(fetchDesc) { if let trip = upcomingTrips.first { ... } } } -
16:24 - Fetch the last transaction efficiently
// Fetch history with sortBy and fetchlimit to get the last token var historyDesc = HistoryDescriptor<DefaultHistoryTransaction>() historyDesc.sortBy = [.init(\.transactionIdentifier, order: .reverse)] historyDesc.fetchLimit = 1 let transactions = try context.fetchHistory(historyDesc) if let transaction = transactions.last { historyToken = transaction.token } -
17:29 - Fetch History after the given token and only for the entities of concern
// Changes AFTER the last known token let tokenPredicate = #Predicate<DefaultHistoryTransaction> { $0.token > historyToken } // Changes for ONLY entities of concern let entityNames = [LivingAccommodation.self, Trip.self] let changesPredicate = #Predicate<DefaultHistoryTransaction> { $0.changes.contains { change in entityNames.contains(change.changedPersistentIdentifier.entityName) } } let fullPredicate = #Predicate<DefaultHistoryTransaction> { tokenPredicate.evaluate($0) && changesPredicate.evaluate($0) } let historyDesc = HistoryDescriptor<DefaultHistoryTransaction>(predicate: fullPredicate) let transactions = try context.fetchHistory(historyDesc)
-
-
- 0:00 - Introduction
SwiftData permet de modéliser et de conserver les données des apps sur toutes les plateformes Apple. Ce framework simplifie la persistance des données, la modélisation et la migration des schémas, la gestion des graphes et la synchronisation CloudKit. L’héritage de classe, une nouvelle fonctionnalité disponible à partir d’iOS 26, permet de construire des graphes de modèles avec héritage.
- 2:11 - Maîtriser l’héritage de classe
L’héritage de classe est un outil puissant et particulièrement utile lorsque les modèles forment une hiérarchie naturelle et partagent des caractéristiques communes. L’héritage permet de créer des sous-classes qui héritent des propriétés et des comportements d’une classe parente, favorisant ainsi la réutilisation du code et le maintien d’une organisation structurée. L’app SampleTrips applique l’héritage pour modéliser différents types de voyages (personnels et professionnels, par exemple). Chaque sous-classe hérite des propriétés essentielles du modèle Trip et ajoute des attributs spécifiques pertinents à son sous-domaine. Cette approche permet une représentation plus personnalisée et plus efficace des données. Utilisez l’héritage avec discernement. L’héritage est approprié lorsque les modèles établissent une relation « is-a » et lorsque les requêtes impliquent à la fois la classe parente et ses sous-classes. Si les modèles ne partagent que des propriétés communes sans hiérarchie naturelle, la conformité à un protocole constitue une approche plus appropriée. Le choix entre l’héritage et la conformité à un protocole dépend également de la complexité des recherches effectuées sur les données.
- 7:39 - Faire évoluer les données avec la migration
Le processus de migration des données de l’app SampleTrips à travers les différentes versions d’iOS illustre l’importance de préserver les données utilisateur lors des mises à niveau. Le schéma de l’app a évolué au fil des versions : iOS 17 a introduit SwiftData ainsi que la version 2.0 du schéma, qui rend les noms de voyage uniques et renomme certaines propriétés. iOS 18 a introduit la version 3.0, qui utilise les macros Index et Unique, et permet de conserver certaines propriétés lors de la suppression. Des étapes de migration personnalisées (MigrationStages) ont été utilisées pour supprimer les doublons. iOS 26 introduit la version 4.0, qui inclut des sous-classes. Une étape de migration légère (MigrationStage) est nécessaire pour passer de la version 3.0 à la version 4.0. Un SchemaMigrationPlan est construit en encapsulant les VersionedSchemas et les MigrationStages dans le bon ordre. Le SchemaMigrationPlan est ensuite appliqué lors de la création du ModelContainer de SampleTrips, permettant une migration fluide à travers toutes les versions précédentes tout en préservant les données utilisateur.
- 11:27 - Personnalisation des données récupérées
Pour explorer l’optimisation des requêtes et des récupérations de données, l’app SampleTrips réintroduit la fonctionnalité de barre de recherche. L’app construit un prédicat basé sur la recherche de l’utilisateur, puis le combine avec le prédicat de classe pour filtrer les voyages. Au-delà de la recherche, ces techniques améliorent les performances de récupération : pendant la migration, seules les propriétés nécessaires sont récupérées à l’aide de propertiesToFetch. La propriété relationshipsToPrefetch est utilisée pour optimiser la navigation dans les relations. La propriété fetchLimit est définie dans le code du widget afin de récupérer uniquement le voyage le plus récent, ce qui améliore l’efficacité.
- 13:54 - Observation des changements apportés aux données
La fonctionnalité Observable de SwiftData permet de réagir aux modifications locales apportées aux PersistentModels. Cependant, toutes les modifications ne sont pas observables. Les modifications provenant d’autres processus, d’actions externes ou de contextes de modèle différents au sein de l’app nécessitent une nouvelle récupération des données, ce qui peut s’avérer coûteux. Pour optimiser les nouvelles récupérations, vous pouvez utiliser la fonctionnalité d’historique persistant de SwiftData. En récupérant le dernier jeton d’historique et en l’utilisant comme repère, vous pouvez créer des prédicats afin de ne récupérer que les entrées de l’historique survenues après ce jeton, et uniquement pour les entités concernées. Cette approche permet à l’app de déterminer si une nouvelle récupération est nécessaire, ce qui évite toute récupération de données inutile et améliore les performances.
- 18:28 - Étapes suivantes
Lorsque vous construisez un graphe de modèles, prenez en compte les implications liées à l’héritage et à la migration. Optimisez les récupérations de données et les requêtes pour améliorer les performances. Utilisez l’observation et l’historique persistant pour suivre les modifications des données.