-
SwiftData: Analiza a detalle la herencia y migración de esquemas
Descubre cómo usar la herencia de clases para modelar tus datos. Aprende a optimizar las consultas y migrar sin problemas los datos de tu app para usar la herencia. Explora la subclasificación para crear gráficos de modelos, crear búsquedas y consultas eficientes e implementar migraciones de esquemas sólidas. Comprende cómo usar el historial observable y persistente para un seguimiento eficiente de los cambios.
Capítulos
- 0:00 - Introducción
- 2:11 - Aprovecha la herencia de clases
- 7:39 - Evolución de datos con la migración
- 11:27 - Adaptación de los datos obtenidos
- 13:54 - Observación de cambios en los datos
- 18:28 - Próximos pasos
Recursos
Videos relacionados
WWDC25
WWDC24
-
Buscar este video…
-
-
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 - Introducción
SwiftData permite modelar y conservar datos de apps en todas las plataformas de Apple. Esta estructura simplifica la persistencia de datos, el modelado y la migración de esquemas, la administración de gráficos y la sincronización de CloudKit. La herencia de clases, una nueva funcionalidad disponible a partir de iOS 26, permite crear gráficos de modelos con la herencia.
- 2:11 - Aprovecha la herencia de clases
La herencia de clases es una herramienta avanzada y particularmente útil cuando los modelos forman una jerarquía natural y comparten características comunes. La herencia permite la creación de subclases que heredan propiedades y comportamientos de una clase principal, lo que promueve la reutilización del código y mantiene una organización estructurada. En la app SampleTrips se aplica la herencia para modelar diferentes tipos de viajes, tanto personales como de negocios. Cada subclase hereda propiedades esenciales del modelo Trip y agrega atributos específicos relevantes a su subdominio. Este enfoque permite una representación de datos más personalizada y eficiente. Usa la herencia con prudencia. La herencia es apropiada cuando los modelos establecen una relación “is-a” y cuando las consultas involucran tanto a la clase principal como a sus subclases. Si los modelos solo comparten propiedades comunes sin una jerarquía natural, la conformidad con el protocolo es un enfoque más adecuado. La elección entre herencia y conformidad con el protocolo también depende de lo detalladas que sean las búsquedas en los datos.
- 7:39 - Evolución de datos con la migración
El proceso de migración de datos de la app SampleTrips en diferentes versiones de iOS es un ejemplo de cómo garantizar la preservación de los datos del usuario durante las actualizaciones. El esquema de la app evolucionó a lo largo de varias versiones. En iOS 17 se introdujo SwiftData y la versión 2.0 del esquema, que hace que los nombres de los viajes sean únicos y se renombren las propiedades. En iOS 18 se agregó la versión 3.0, que utiliza índices y macros únicas, y conserva las propiedades al momento de la eliminación. Se usaron MigrationStages personalizados para la eliminación de duplicados. En iOS 26 se presenta la versión 4.0, que incluye subclases. Se necesita un MigrationStage ligero de la versión 3.0 a la 4.0. Un SchemaMigrationPlan se construye encapsulando los VersionedSchemas y MigrationStages en el orden correcto. Luego, se aplica SchemaMigrationPlan al crear ModelContainer para SampleTrips, lo que permite una migración sin problemas a través de todas las iteraciones anteriores y, al mismo tiempo, se preservan los datos del usuario.
- 11:27 - Adaptación de los datos obtenidos
Para explorar la optimización de consultas y búsquedas, la app SampleTrips reintroduce la funcionalidad de la barra de búsqueda. Construye un predicado basado en la búsqueda del cliente y luego esta se combina con el predicado de clase para filtrar viajes. Estas técnicas mejoran el rendimiento de la búsqueda. Durante la migración, solo se obtienen las propiedades necesarias mediante propertiesToFetch. relationshipsToPrefetch se usa para optimizar el recorrido de relaciones. fetchLimit se configura en el código del widget para recuperar solo el viaje más reciente, lo que mejora la eficiencia.
- 13:54 - Observación de cambios en los datos
La funcionalidad Observable de SwiftData te ayuda a reaccionar a los cambios locales realizados en PersistentModels. Pero no todos los cambios son observables. Los cambios de otros procesos, acciones externas o diferentes contextos de modelos dentro de la app requieren una nueva búsqueda, lo que puede resultar costoso. Para optimizar esto, puedes usar la funcionalidad de historial persistente de SwiftData. Al obtener el último token del historial y usarlo como marcador, puedes crear predicados para obtener únicamente las entradas del historial que se registraron después del último token y para entidades específicas de interés. Este enfoque permite que la app determine si es necesaria una nueva búsqueda, lo que evita la recuperación innecesaria de datos y mejora el rendimiento.
- 18:28 - Próximos pasos
Al crear un gráfico de modelo, considera las implicaciones de herencia y migración. Mejora los buscadores de datos y las consultas para mejorar el rendimiento. Usa la observación y el historial persistente para llevar un registro de los cambios en los datos.