
-
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…
Bonjour, je suis Rishi Verma, ingénieur au sein de l’équipe SwiftData. Je vous souhaite la bienvenue dans « SwiftData: Dive into inheritance and schema migration ». SwiftData a été introduit dans iOS 17 et permet de modéliser et de conserver les données de l’app en Swift sur toutes les plates-formes Apple. Il permet d’écrire un code rapide, efficace et sûr en exploitant les fonctionnalités modernes du langage Swift. Cette vidéo continue avec une introduction à l’héritage de classe et explique quand l’héritage est le bon choix. Avec l’adoption de l’héritage et l’évolution du schéma, nous allons aborder les stratégies de migration utilisées pour préserver les données, puis nous verrons plusieurs façons d’adapter les requêtes SwiftData pour une performance optimale. Enfin, nous allons observer les changements apportés aux modèles, localement ou à distance. Depuis quelques versions, nous utilisons une app bien connue, SampleTrips. Il s’agit d’une app développée avec SwiftUI pour suivre tous les voyages que j’ai prévus. Pour utiliser SwiftData avec les modèles de cette app, il suffit d’importer le framework et d’annoter chaque modèle avec la macro Model.
Dans la définition de l’app, nous ajoutons le modificateur modelContainer sur le WindowGroup, ce qui informe toute la hiérarchie des vues du modèle Trip. Avec le modelContainer configuré, je peux maintenant mettre à jour ma vue pour utiliser la macro Query. Supprimons ces données statiques et remplissons plutôt la vue en utilisant la macro Query, qui générera le code pour récupérer les voyages depuis le modelContainer.
C’est tout ! L’app enregistre tous les voyages que je crée et s’intègre parfaitement à mes vues SwiftUI. SwiftData offre une gestion simplifiée de la persistance, mais aussi la modélisation et la migration de votre schéma, la gestion des graphes, la synchronisation avec CloudKit, etc. Dernière nouveauté de SwiftData : la prise en charge de l’héritage de classe. Nouveauté d’iOS 26 : la possibilité de créer un graphe de modèles exploitant l’héritage. L’héritage de classe est un outil puissant. Voyons dans quels cas c’est l’outil le plus adapté. L’héritage est adapté aux modèles hiérarchiques aux caractéristiques communes. Le modèle Trip contient les propriétés destination, startDate et endDate, essentielles pour savoir où et quand partir. Ainsi, toute sous-classe de Trip héritera automatiquement de ces propriétés et des comportements partagés définis dans Trip. Le modèle Trip couvre un vaste domaine. Nous faisons toutes sortes de voyages au cours de notre vie. Une nouvelle sous-classe de Trip doit représenter un sous-domaine naturel du modèle Trip.
Dans notre app SampleTrips, la plupart des voyages relèvent de deux sous-domaines naturels : personnels et professionnels. Avec ces 2 nouveaux modèles exprimant les sous-domaines naturels d’un voyage, je peux ajouter des propriétés et comportements spécifiques à chacun. Pour les voyages personnels, je vais ajouter une énumération indiquant la raison du voyage. Pour les voyages professionnels, je vais ajouter une propriété perdiem pour suivre mes dépenses lors de mon prochain déplacement. Faisons cela dans l’app SampleTrips pour une expérience plus riche. Voici notre classe Trip avec les propriétés à partager avec ses sous-classes. Ajoutons 2 sous-classes à notre app Trips : une pour les voyages professionnels et une pour les voyages personnels. Je dois aussi les annoter avec @available sur iOS 26 pour correspondre à la prise en charge de l’héritage dans SwiftData. Ajoutons maintenant les propriétés spécifiques au sous-domaine à nos sous-classes. Je peux ajouter un perdiem au BusinessTrip et définir sa valeur initiale. Et pour PersonalTrip, on ajoute une énumération Reason pour indiquer la raison du voyage. Ah oui, dernière étape : mettre à jour notre schéma pour inclure les nouvelles sous-classes. Ajoutons BusinessTrip et PersonalTrip au modificateur modelContainer, et c’est parti ! L’app SampleTrips est prête à exploiter les nouveaux voyages personnels en bleu et professionnels en vert, sans code supplémentaire. L’héritage de classe est un outil puissant, mais il ne convient pas à tous les cas. Voyons quand il est pertinent d’utiliser l’héritage.
Dans certains cas, l’héritage est la solution idéale. Si vos modèles reflètent naturellement une hiérarchie avec des caractéristiques communes à étendre, l’héritage peut être un bon choix, car vos types forment alors une relation de type « est un ».
En utilisant des modèles hérités, on sait qu’un voyage personnel est un Trip. Ainsi, lorsqu’on travaille avec le type Trip, comme dans la requête affichée ici, on peut s’attendre à retrouver tous les types de voyages : personnels, professionnels, ou de la classe Trip elle-même. Nos voyages sont représentés par des avions, avec les mêmes couleurs que notre UI, qui volent du modelContainer vers le modelContext à l’origine de la requête. Cependant, l’héritage ne doit pas être exploité pour partager des caractéristiques communes entre les modèles. Si l’on créait une sous-classe pour tous les modèles ayant une propriété name, on aurait une hiérarchie contenant de nombreux sous-domaines ne partageant qu’une seule propriété, tandis que toutes les autres caractéristiques resteraient propres à chaque sous-domaine. Comme ces sous-domaines ne forment pas une hiérarchie naturelle, il vaut mieux les représenter par une conformité à un protocole. La conformité à un protocole permet à des domaines distincts de partager des comportements sans caractéristiques superflues. L’héritage peut aussi être utile selon la façon dont vous interrogez ou récupérez vos modèles.
Il y a plusieurs façons d’interroger des données, ici on utilise la macro Query pour récupérer tous les voyages depuis le modelContainer et alimenter nos vues. C’est un exemple de recherche en profondeur.
Si l’on utilise uniquement des recherches en profondeur, en ne récupérant que des objets de type Trip, il vaut mieux représenter PersonalTrip ou BusinessTrip comme une propriété de Trip plutôt que comme une sous-classe. Mais, si vos requêtes ciblent uniquement les types de sous-classes finales, on parle alors de recherche en surface. Il est alors préférable d’aplatir nos modèles, puisque le type Trip n’est jamais interrogé ni utilisé directement. Si vous combinez recherches en profondeur et en surface, l’héritage est utile pour afficher tous les voyages ou un sous-type comme PersonalTrips selon le contexte. Voyons comment mettre à jour notre app Trips pour n’afficher que les voyages personnels ou professionnels.
Utilisons un contrôle segmenté pour afficher tous les voyages ou filtrer par sous-classe.
Le segment sélectionné permet de définir un prédicat qui filtre les types via le mot-clé « is ». Par exemple, ici je vérifie s’il s’agit d’un PersonalTrip. On fournit ensuite le prédicat et on trie par startDate pour initialiser notre Query. Voyons le résultat dans l’app. On commence par la vue des voyages qui affiche tous mes Trips. Je peux ensuite filtrer l’affichage par sous-classe spécifique. Trop fort ! Voilà comment tirer parti de l’héritage de classe dans iOS 26. Mais, je n’ai pas encore terminé. Nous avons apporté des modifications importantes à notre schéma, il faut donc réfléchir à leurs conséquences pour l’app et à la manière de migrer nos données. L’app SampleTrips a connu plusieurs évolutions au cours des dernières versions. Prenons un moment pour créer des schémas versionnés et un plan de migration, pour que l’app conserve les données de l’utilisateur lors de la mise à jour de SampleTrips.
Tout a commencé dans notre première vidéo avec SwiftData sur iOS 17, illustré par l’exemple des voyages. Dans ces vidéos d’introduction, nous avons appris à rendre le nom d’un voyage unique et à renommer une propriété pour conserver les données lors d’une migration.
Pour iOS 17, nous avons créé un schéma versionné en 2.0, avec un modèle Trip modifié : nom unique et dates de début et de fin renommées.
Puis, nous avons ajouté une étape de migration personnalisée pour dédupliquer les voyages existants. Ici, nous avons utilisé la fonction fetch de ModelContext pour récupérer tous les voyages et les dédupliquer.
Dans iOS 18, nous avons utilisé les macros index et unique, tout en indiquant quelles propriétés conserver lors d’une suppression.
Cela nous permet d’identifier nos modèles après leur suppression du magasin de données.
Dans notre schéma versionné pour iOS 18, on le marque en version 3 et on y consigne les modifications du modèle Trip. Nouvelle utilisation des macros unique et index pour garantir la déduplication des données et optimiser les recherches et requêtes. Nous avons aussi annoté ces propriétés avec preserved value on deletion pour pouvoir identifier un voyage supprimé lors de l’utilisation de l’historique persistant.
Nous avons aussi ajouté une étape de migration personnalisée pour dédupliquer les voyages entre les versions 2 et 3. Dans iOS 26, nous ajoutons la version 4 avec des sous-classes et une étape de migration légère. Dans notre schéma actuel, on le marque en version 4 et on y inclut tous les modèles, y compris les nouvelles sous-classes. Et puisque nos sous-classes sont annotées pour iOS 26 ou version ultérieure, notre schéma de version l’est aussi. Nous devons ajouter une étape de migration légère de la version 3 à la version 4, avec la même disponibilité qu’auparavant. Une fois notre dernier schéma de version et l’étape de migration définis, nous pouvons les regrouper dans un plan de migration, qui précise l’ordre des versions et des étapes à exécuter. Le plan de migration du schéma consiste en un tableau de schémas listés dans l’ordre de leur publication. Lorsque iOS 26 est disponible, on ajoute le nouveau schéma avec les sous-classes, ainsi qu’un tableau d’étapes de migration permettant de passer d’une version à l’autre. Et voilà comment construire un plan de migration de schéma. Maintenant que notre schéma de version et son plan de migration sont prêts, l’étape suivante consiste à les utiliser lors de la création du modelContainer pour SampleTrips. Revenons à notre modificateur modelContainer et mettons-le à jour pour qu’il utilise un conteneur avec un plan de migration de schéma. On commence par ajouter une nouvelle propriété container à l’app, dans laquelle on construit notre schéma versionné (version 4) et on fournit le plan de migration à l’initialiseur de ModelContainer. On met à jour le modelContainer pour utiliser ce nouveau conteneur compatible avec la migration. Ainsi, les mises à jour de SampleTrips tirant parti de l’héritage peuvent migrer facilement à travers les versions précédentes, tout en préservant les données utilisateur. Maintenant que la migration est en place, il est temps d’optimiser les requêtes et récupérations utilisées pour alimenter nos vues et étapes de migration. Nous avons mis à jour la requête avec le segment choisi via un prédicat. Mais dans une autre vidéo, nous avions aussi une barre de recherche. Réintégrons-la et passons directement à la gestion du texte saisi par l’utilisateur.
Nous commençons par construire un prédicat avec le searchText fourni. On commence par vérifier si le texte est vide. Sinon, on crée un prédicat composé pour vérifier si le nom ou la destination du voyage contient le texte saisi. Puis, nous construisons un prédicat composé avec le prédicat de recherche et le prédicat de classe. Enfin, on met à jour l’initialiseur de notre Query pour utiliser le nouveau prédicat composé. Avec cette mise à jour, je peux utiliser la barre de recherche pour filtrer les voyages, puis affiner encore avec le contrôle segmenté. Le filtrage et le tri ne sont que quelques moyens de personnaliser les requêtes et récupérations. Voyons d’autres façons de personnaliser les extractions SwiftData. Voici notre étape de migration personnalisée de la version 1 à la 2. Nous allons utiliser le bloc willMigrate pour récupérer tous les Trips. Cependant, dans ma logique de déduplication, je n’accède qu’à une seule propriété : name, car c’est la propriété unique en version 2, utilisée pour éviter les doublons. Puisque name est la seule propriété utilisée, je peux mettre à jour le fetchDescriptor avec propertiesToFetch pour ne récupérer que name, ce qui évite de charger inutilement d’autres données lors de la migration. De plus, si l’on sait qu’une relation sera parcourue, ici, je prévois de réassigner un hébergement en cas de doublon, on peut aussi optimiser en utilisant relationshipsToPrefetch. Ajoutons ici la relation livingAccommodation.
Maintenant que nous utilisons le préchargement des propriétés, nous pouvons optimiser le code du widget existant dans SampleTrips pour le rendre plus performant. Dans le widget SampleTrips, une requête récupère le voyage le plus récent. Mais on peut l’optimiser pour ne récupérer qu’une seule valeur. Actuellement, le widget utilise uniquement le 1er résultat de la récupération. On peut rendre cela plus efficace en définissant une fetch limit.
En définissant une fetch limit, le widget récupère uniquement le premier voyage correspondant au prédicat, sans se soucier d’un grand nombre de vacances planifiées. Maintenant que nos requêtes et récupérations sont optimisées, voyons comment savoir quand le modèle a changé. Tous les modèles persistants sont Observable, ce qui permet d’utiliser withObservationTracking pour réagir aux modifications des propriétés qui nous intéressent. Pour observer les changements des dates de début et de fin d’un voyage, on peut ajouter une fonction datesChangedAlert qui déclenche une alerte lorsque l’utilisateur modifie ces dates.
On peut observer de nombreuses modifications locales apportées à nos PersistentModels, ce qui est très utile pour suivre les changements effectués en local. Pour en savoir plus sur les nouveautés d’Observable, regardez « What’s New in Swift ». Seules les modifications effectuées sur vos modèles dans le processus en cours sont Observable. Celles provenant d’un autre processus (comme un widget, une extension ou un autre modelContainer de votre app) ne le sont pas. Les modifications locales ou internes à l’app surviennent lorsque plusieurs modelContext partagent le même modelContainer. Ces modelContext détectent mutuellement leurs modifications, et les Query les appliquent automatiquement. En revanche, si vous utilisez les API de récupération (fetch) du modelContext, les modifications d’un autre modelContext ne seront visibles qu’après un nouveau fetch.
Une action externe peut aussi modifier vos données : un widget qui enregistre des infos ou une autre app écrivant dans le conteneur partagé de votre App Group. Ces modifications mettront automatiquement à jour la vue alimentée par la Query. Mais, les récupérations manuelles devront être relancées pour voir les changements. Une récupération peut être coûteuse, surtout si rien d’important pour le traitement du modèle n’a changé.
Heureusement, SwiftData conserve un historique des modifications. On peut savoir quels modèles ont été modifiés, quand, par qui, et même quelles propriétés ont été mises à jour. Nous utilisons aussi preservedValueOnDeletion sur plusieurs propriétés de Trip, ce qui permet à l’historique de conserver une trace lors d’une suppression, afin d’identifier le voyage supprimé. Pour en savoir plus, regardez « Track Model Changes with SwiftData History » de la WWDC24.
Utilisons l’historique persistant pour savoir si un nouveau fetch est nécessaire. La première étape consiste à récupérer le dernier jeton d’historique du conteneur. Il sert de repère indiquant jusqu’où nous avons lu la base de données pour la dernière fois. Comme un marque-page indiquant où nous nous sommes arrêtés dans notre livre préféré. On lance une récupération avec un descripteur pour la transaction d’historique par défaut. Cependant, si l’historique persistant est volumineux, on risque de récupérer beaucoup de données juste pour obtenir le dernier jeton. iOS 26 apporte une nouveauté : la possibilité de récupérer l’historique en le triant avec sortBy. Nous pouvons spécifier n’importe quelle propriété de la transaction, comme author ou transactionIdentifier, pour trier le résultat de l’historique. Adoptons le nouveau tri en utilisant transactionIdentifier en ordre inverse, pour obtenir les transactions les plus récentes en 1er. Ensuite, comme seule la 1ère transaction (la plus récente) nous intéresse, limitons le résultat à 1. C’est tout ce qu’il faut pour récupérer efficacement le dernier jeton d’historique et le stocker. Gardons ce jeton pour plus tard, pour de prochaines récupérations d’historique. Désormais, à chaque changement, par exemple, une mise à jour par le widget, une entrée s’ajoute à l’historique. L’app vérifie alors si des modifications depuis le dernier jeton sont pertinentes. Avec un jeton stocké, nous pouvons créer un prédicat pour récupérer uniquement l’historique postérieur à ce jeton. Pour ne récupérer que les changements pertinents, on définit les entités dont on veut suivre les modifications. Ici, on souhaite uniquement savoir si un voyage ou son hébergement a été modifié, par exemple si le widget a confirmé notre lieu de séjour. On utilise les noms des entités dans le prédicat de changements pour filtrer l’historique selon les types souhaités. Enfin, en combinant le prédicat du jeton et celui des changements, on crée un prédicat composé. Ainsi, lors de la récupération de l’historique, on obtient uniquement les entrées postérieures à notre jeton et concernant les entités qui nous intéressent. Avec une récupération d’historique optimisée, on peut éviter un nouveau fetch s’il n’y a pas de changements pertinents. Heureusement, l’historique de SwiftData facilite cette tâche. Ainsi, nous savons comment observer les modifications locales et distantes de nos modèles et données. J’espère que cette vidéo vous aidera à exploiter SwiftData pour vos besoins de persistance. En construisant votre graphe de modèles, pensez à l’héritage et à ses impacts sur la migration au fur et à mesure de son évolution. Pour récupérer vos données, créez des récupérateurs et des requêtes plus riches et performants. Savoir quand vos données changent est précieux. L’observation et l’historique persistant vous facilitent la tâche. C’est tout pour moi. Je vous souhaite un excellent voyage.
-
-
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.