View in English

  • Global Nav Open Menu Global Nav Close Menu
  • Apple Developer
Search
Cancel
  • Apple Developer
  • News
  • Discover
  • Design
  • Develop
  • Distribute
  • Support
  • Account
Only search within “”

Quick Links

5 Quick Links

Videos

Open Menu Close Menu
  • Collections
  • Topics
  • All Videos
  • About

Back to WWDC25

Streaming is available in most browsers,
and in the Developer app.

  • About
  • Transcript
  • Code
  • SwiftData: Dive into inheritance and schema migration

    Discover how to use class inheritance to model your data. Learn how to optimize queries and seamlessly migrate your app's data to use inheritance. Explore subclassing for building model graphs, crafting efficient fetches and queries, and implementing robust schema migrations. Understand how to use Observable and persistent history for efficient change tracking.

    Chapters

    • 0:00 - Introduction
    • 2:11 - Harness class inheritance
    • 7:39 - Evolving data with migration
    • 11:27 - Tailoring fetched data
    • 13:54 - Observing changes to data
    • 18:28 - Next steps

    Resources

    • Adopting SwiftData for a Core Data app
    • Building rich SwiftUI text experiences
    • SwiftData
      • HD Video
      • SD Video

    Related Videos

    WWDC25

    • What’s new in SwiftUI

    WWDC24

    • Track model changes with SwiftData history
  • Search this video…

    Hello, my name is Rishi Verma, and I’m an engineer on the SwiftData team. And I would like to welcome you to “SwiftData: Dive into inheritance and schema migration.” SwiftData was introduced in iOS 17 and allows you to model and persist your app’s data in Swift across all of Apple's platforms. It lets you write code that is fast, efficient, and safe by harnessing modern Swift language features.

    And that continues in this video with an introduction to harnessing class inheritance and learning when inheritance is the right choice. With the adoption of inheritance and the evolution of the Schema, we discussed the migration strategies used to preserve data, after which we will explore a few ways to tailor SwiftData fetches and queries for optimal performance. And lastly, a bit on how to observe changes to models done locally and remotely. For a few releases now, we have been using a familiar app, SampleTrips. This is an app written in SwiftUI to keep track of all the different trips I have planned.

    To use SwiftData with the models in this app, I just need to import the framework and decorate each model with the Model macro.

    And in the app’s definition, we add the modelContainer modifier on the WindowGroup, which tells the entire view hierarchy about the model Trip.

    With the modelContainer wired up, I can now update my view to harness the Query macro. Let’s remove this static data and instead populate the view using the Query macro, which will generate the code to fetch the trips from the model container.

    And that's it. The app now persists all of the trips that I create and fits perfectly into my SwiftUI views.

    SwiftData not only provides persistence with ease, but modeling and migration of your schema, graph management, synchronization with CloudKit, and so much more. And the latest new feature to come to SwiftData is class inheritance. New in iOS 26 is the ability to build a model graph that harnesses inheritance.

    Class Inheritance is a powerful tool. Let’s learn when it’s the right tool for the job.

    Inheritance works well when your models form a natural hierarchy and share characteristics. The Trip model has a destination, startDate, and endDate, properties that every trip needs so we know where and when we're going.

    And so any new subclass of Trip will already have these properties and any of the other shared behaviors defined in Trip.

    The Trip model is also a broad domain. There are so many types of trips that we take in our lives. A new subclass of Trip should be of a natural subdomain that fits within the broader domain Trip.

    In our SampleTrips app, many of the trips fall into two natural subdomains: personal and business trips. With these two new models expressing natural subdomains of a trip, I want to add properties and behaviors that are specific to these subclasses.

    For the personal trips, I will add an enumeration that captures the reason why I might be going on that particular trip. And for my business trip, I want to add a property to note my perdiem, so I know how much I should spend on my next work adventure. Let’s do this in the SampleTrips app and create a richer experience.

    Here is our Trip class with the properties we want to share with our subclasses. Let’s add two new subclasses to our Trips app, one for my business trips and one for my personal trips. And I also need to decorate them with the @available on iOS 26 or later so that it aligns with SwiftData’s inheritance support. Now let’s add the subdomain specific properties to our subclasses. I can add a perdiem to the BusinessTrip and set it’s initial value. And for our PersonalTrip, we add a Reason enumeration to capture the reason why we’re going on our personal trip.

    Oh wait, one last thing we need to do is update our schema to include our new subclasses. Let’s add Business and Personal Trip to the modelContainer modifier and we’re ready to go.

    The SampleTrips app is ready to harness the new personal trips in blue and business trips in green with no other special code needed.

    While class inheritance is a powerful tool, it is not for every problem. Let's discuss when you would utilize inheritance.

    There are a few scenarios where inheritance is the right path. If your models naturally express a hierarchical relationship and have common characteristics that you want to extend, then inheritance may be the correct choice as your types form an “is-a” relationship.

    When we use our inherited models, we know that a personal trip IS-A trip, meaning whenever I work with a type Trip, such as in the Query for trips in the view here, I can expect to find all types of trips, including personal and business trips, as well as just instances of the parent class Trip. Here, we see our trips denoted as planes with the same coloring as our UI, flying from the model container to the model context backing the query.

    However, inheritance should not be harnessed to share common characteristics among models. For example, if we were to subclass all of our models that have a property called name, our class hierarchy would contain many subdomains that only share a single property for a common purpose and all of the other characteristics are isolated to their subdomains.

    And because these subdomains do not form a natural hierarchy, they are better expressed as a protocol conformance.

    The protocol conformance allows for the distinct domains to share behaviors, but not other characteristics that are unrelated. Another reason to use inheritance depends on how you query or fetch your models.

    There are several ways to query for data, and we currently harness the Query macro to fetch all trips from the model container to drive our views. This is an example of a deep search.

    If we only harness deep searches, meaning that we always fetch all trips and utilizes only the Trip type, then we should consider Personal or Business Trip to be a property on Trip rather than a subclass.

    However, if your queries or fetches only ever fetch the leaf class types, this is known as a shallow search. In this scenario, we would consider flattening our models, since Trip is never queried or utilized as a type. But if you utilize deep and shallow searches, inheritance will help as you’re often going to search for all trips, or specific subtype, such as PersonalTrips, to drive a view tailored to that type. Let’s take a moment to see how we can update our Trips app to show only the personal or business trips.

    Let’s utilize a segmented control so we can show all our trips and then our specific subclasses.

    The selected segment can be used to drive a predicate that will determine if the class is of a particular type via the 'is' keyword. For example, here I’m checking whether it is a PersonalTrip. We then provide the predicate and sort by the trip startDate to initialize our Query. Let's check it out in the app. It starts at the trip view so I can see all my Trips. Then I can narrow the views to the specific subclasses. Awesome sauce! That’s how we harness class inheritance in iOS 26. However, I'm not done yet. We just made some major changes to our schema and we should consider what that means for our existing app and how to migrate our data.

    The SampleTrips app has gone through several evolutions over the past few releases. Let’s take a moment to capture those all here in versioned Schemas and a schema migration plan so our app will preserve the user’s data on upgrade to our latest SampleTrips app.

    It all started with our first video, where we introduced SwiftData in iOS 17 and used Trips to guide the adoption.

    And through those introductory videos, we learned to make a trip’s name unique and how to alter the original name of a property to preserve the data on migration.

    For iOS 17, we constructed our versioned schema with a new version identifier 2.0 and show the changed model Trip with a unique name and renamed start and end dates.

    Next, we added a custom migration stage so that we could deduplicate the existing Trips.

    Here, we utilized the ModelContext’s fetch function so that we can fetch all the trips in order to deduplicate them.

    In iOS 18, we harness the index and unique macros while also marking which properties we wanted to preserve on deletion.

    This allows us the ability to identify our models after deletion from the data store.

    In our versioned schema for iOS 18, we mark it as version 3 and capture the changes to the Trip model. New usage of the unique and index macros to ensure our data is deduplicated and performant for fetches and queries. We also decorated those same properties with preserved value on deletion, so we can identify the deleted trip when we’re consuming persistent history.

    We also added another custom migration stage to once again, deduplicate the trips when migrating from version 2 to version 3. Now in iOS 26, we will add version 4 with subclasses and a lightweight migration stage.

    For our current version schema, we mark it as version 4 and list all of the models in our schema with our new subclasses.

    And because our subclasses were decorated with iOS 26 or later, so is our version schema.

    And we need to add a lightweight migration stage from version 3 to version 4 with the same availability needed before.

    And with our final version schema and migration stage constructed, we can encapsulate all of these into a schema migration plan so that we can provide the ordering of the version schemas and the migration stages to run.

    The schema migration plan consists of an array of schemas in the order they were released. And when iOS 26 is available, we add our newest schema with subclasses, and then an array of migration stages so that we can go from one release to the next. And that is how we build our schema migration plan. Now that we’ve built up our version schema and its corresponding schema migration plan, The next step is to harness them when creating our model container for SampleTrips.

    Let’s go back to our modelContainer modifier and update it to use a model container with a schema migration plan.

    We start by adding a new container property to the application in which we will construct our versioned schema with version 4 and provide the schema migration plan to the ModelContainer initializer.

    And now we update our modelContainer modifier to use the new migratable container.

    With that all in place, we’ve made sure that the updates to SampleTrips to harness inheritance can easily migrate through the various iterations we shipped before, all while preserving the client's data. Now that we've handled migration, it’s time to consider where we can improve queries and fetches we utilize to drive our views and migration stages.

    We last updated the query with the chosen segment via Predicate.

    but in a past video, we also had a search bar. Let’s add that back and jump straight into handling the search text a client has entered.

    We start by building a predicate with the searchText provided. First, we check if the text is empty. And if not, we build a compound predicate to see if the trip’s name or destination contains the given text.

    Next, let’s build a compound predicate with the search predicate and the class predicate. And lastly, we update our Query initializer to take the new compound predicate.

    With that update, I can tap the search bar, enter some text to filter the trips, and even narrow it down further with a segmented control.

    Filtering and sorting are just a few ways that we can tailor queries and fetches.

    Let’s explore a few other ways we can tailor SwiftData fetches.

    Here’s our custom migration stage from version 1 to version 2. we will use the willMigrate block to fetch all of the Trips. However, in my deduplication logic, I only ever access the single property, name, since that is the unique property in version 2, and I use it to ensure there are no other duplicates. Since name is the only property I access I can update the fetchDescriptor to use propertiesToFetch with the name so that our Trip models are only packing the data we need during migration.

    Additionally, if we know we may traverse a particular relationship, in this case, I know that I will reassign a living accommodation if I find a duplicate, we can do the same enhancement by utilizing relationshipsToPrefetch. Let’s add the livingAccommodation relationship here.

    Now that we’ve adopted prefetch properties, we can also update the existing widget code we have in SampleTrips to be a bit more performant.

    In the SampleTrips widget, we have a query for the most recent trip. However, it can be improved so we only fetch a single value. Currently, the widget code only harnesses the first result of the fetch. But we can make this more efficient by setting the fetch limit.

    By setting the fetch limit, the widget will get the first trip to match the predicate and will not need to worry about a scenario when I have way too many vacations planned in the future. With our queries and fetches improved, let’s explore how to know when your model has changed.

    All persistent models are Observable, and so we can utilize withObservationTracking to react to changes made to properties of interest in our models.

    If we wanted to observe changes to our trip start and end dates, we could add this function datesChangedAlert so that if the user changes the dates, we post an alert.

    We can observe many of the local changes made to our PersistentModels this way, and it’s very useful for changes made locally. For more details in the latest on Observable, Check out What’s new in Swift. However, not all changes are Observable, only those made to your models in process, not those made to your data store from another process, such as widget or extension, or even another model container within your app. Local or internal changes to your app are when you have several model contexts using the same model container. These other model contexts can see each other’s changes, and in the case of Query, those changes are automatically applied.

    However, if you’re harnessing the fetch APIs of the model context, changes made in another model context will not be seen until a refetch is triggered.

    Additionally, an external action can also change your data, such as a widget saving or another app writing to your shared App Group container. These changes will automatically update the Query backed view. However, fetch usages will need to refetch again.

    Refetching can be expensive, particularly if nothing of interest to our model processing has changed.

    Fortunately for us, SwiftData has history that has persisted. We can know which models have changed, when they’ve changed, and who changed the models, even which properties were updated. We also harness preservedValueOnDeletion on several properties of Trip, so when a trip is deleted, history will have a tombstone that we can parse to identify the deleted trip. For more on history, see "Track Model Changes with SwiftData History" from WWDC24.

    Let’s harness persistent history to know if we need to refetch. The first thing we would want to do is fetch the latest history token from the container.

    So we can use this token as our marker for where we last read from the database. Very much like a bookmark for where we left off reading in our favorite book.

    We set up a history fetch with a history descriptor for the default history transaction. However, if we have a lot of persistent history, we could be fetching a lot of data, only to grab the last one for our token. Well, fortunately, new in iOS 26 is the ability to fetch history with a sortBy. We can specify any of the transaction properties, such as author or transactionIdentifier, as a key pass to sort our history result by. Let’s adopt the new sort by and set it to the transactionIdentifier, but in reverse, so the newest transactions are first. Then, we only care about the first transaction that is the newest, so let’s limit our result to 1. And that is all we need to do to performantly fetch the latest history token and store it. Let's save this token for later and use it on future history fetches.

    Now, when a new change occurs, such as the Widget updating a trip, a new entry is added to our history, and our app can fetch history to see if any changes since our last token are of interest. Now that we have a stored history token, we can build a predicate that only fetches history after that token. And in order to only find the changes we care about, we build up the entities we want to know that have changed. Here, we only want to know if a trip has changed or its living accommodations in case the widget confirmed where we are staying.

    And use the entity names in the changes predicate to filter history changes for our desired Types. Lastly, with our token predicate and change predicate, we build our compound predicate.

    And with these changes in place, when we fetch history, we only get back the history after our token and for entities we are currently concerned with processing.

    And with a better history fetch, we can avoid refetching if there are no changes of interest. Thankfully, SwiftData history makes this easy. And with that, we know how to observe local and remote changes to our models and data.

    I hope you found this video informative and harness SwiftData for your persistence needs. While building your model graph, consider if inheritance is the right fit and what the migration implications are as your graph evolves. When it comes to getting your data, build richer and more performant fetchers and queries. And knowing when your data has changed can be invaluable. Observation and persistent history have you covered. And that’s all I got.

    I hope you enjoy your trip.

    • 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)

Developer Footer

  • Videos
  • WWDC25
  • SwiftData: Dive into inheritance and schema migration
  • Open Menu Close Menu
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    Open Menu Close Menu
    • Accessibility
    • Accessories
    • App Extensions
    • App Store
    • Audio & Video
    • Augmented Reality
    • Design
    • Distribution
    • Education
    • Fonts
    • Games
    • Health & Fitness
    • In-App Purchase
    • Localization
    • Maps & Location
    • Machine Learning
    • Open Source
    • Security
    • Safari & Web
    Open Menu Close Menu
    • Documentation
    • Tutorials
    • Downloads
    • Forums
    • Videos
    Open Menu Close Menu
    • Support Articles
    • Contact Us
    • Bug Reporting
    • System Status
    Open Menu Close Menu
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles
    • Feedback Assistant
    Open Menu Close Menu
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program
    • News Partner Program
    • Video Partner Program
    • Security Bounty Program
    • Security Research Device Program
    Open Menu Close Menu
    • Meet with Apple
    • Apple Developer Centers
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Academies
    • WWDC
    Get the Apple Developer app.
    Copyright © 2025 Apple Inc. All rights reserved.
    Terms of Use Privacy Policy Agreements and Guidelines