Persistent History Tracking in Core Data
At WWDC ’17, Apple introduced a number of new Core Data features, one of which is Persistent History Tracking or
NSPersistentHistory
. But as of the time of writing, its API is still undocumented. Thus, the only real reference is the What’s New in Core Data WWDC session.Since Persistent History Tracking makes sharing an
NSPersistentStore
across multiple processes and is one of my favorite new Core Data features, it is unfortunate that it mostly seems to fall of the radar.The purpose of this post is to give a real-world example on how to use it and what makes it so great.
That was written a year and a half ago, and NSPersistentHistory
remains a really cool feature that’s under-discussed and under-documented. Some resources I’ve found are:
- WWDC 2017 Session 210: What’s New in Core Data
- WWDC 2019 Session 230: Making Apps with Core Data
- Consuming Relevant Store Changes
- Persistent History
Here are some things I figured out by exploring:
- The history is stored directly in the same SQLite database(s) as the persistent store.
- It uses tables that look kind of like Core Data tables, only with a different prefix.
- But you couldn’t create them yourself using Core Data, since the same column can store the primary key for different types of entities (you would think
NSObjectIDAttributeType
could do that, but it actually can’t be used in stores), and likewise for the columns that store the tombstone values. - The tables are updated using SQLite triggers, which are again not directly exposed in Core Data (though this year’s new derived attributes also use them).
- The triggers are fired for all database changes, so unlike the managed object context’s change tracking, they also work for batch updates and deletions (and, presumably, the forthcoming batch insertions).
- But they don’t tombstone attributes that use
allowsExternalBinaryDataStorage
, even for small values that are stored in the database. In my opinion, Core Data should report an error if you try to use a model that’s configured in this way. - The tables look very compact, with repeated string values interned and the list of modified columns stored as a bit vector.
- Core Data automatically updates the schema of the history tracking tables when you do a migration.
- Enabling history tracking does not change the version of your model. But, in practice, you’ll get incorrect results if you don’t enable it consistently.
- Setting an attribute to be preserved after deletion (i.e. for the tombstone) does change the model’s version hash, however.
- There’s no public API to set this flag on an attribute in the model, only a checkbox in Xcode. However, you can use key-value coding to set or query
NSPropertyDescription.preserveValueOnDeletionInPersistentHistory
. - So, overall, it seems tricky to use persistent history on a store that will be shared with OS versions that don’t support history tracking. You might have to roll your own in that case.
- Querying and pruning the history works as you would expect.
- The
NSPersistentHistoryTransaction.objectIDNotification()
does not generate aNSManagedObjectContextDidSaveNotification
, but rather a privateNSManagedObjectContextDidSaveObjectIDsNotification
notification. - Rather than containing full objects under keys like
NSUpdatedObjectsKey
, it contains object IDs under keys likeupdated_objectIDs
. This is a bit unexpected, becauseNSManagedObjectContext
is already documented to supportNSManagedObjectID
orNSURL
objects under theNSUpdatedObjectsKey
key. - In any case, you get IDs because it isn’t storing the changed values. Instead, when merging, it fetches the latest values from the store.
- This makes sense given the data model, but it means that, perhaps counterintuitively, merging will update all the attributes, not just those those changed in the transaction that generated the notification. And they’ll be updated to the current values, which may be much newer than the ones at the time of the transaction. This is not version control, just a way to see what has changed.
Update (2019-08-22): Deeje Cooley:
I incorporated Persistent History Tracking into CloudCore, an open-source CoreData-CloudKit sync engine, specifically to support offline sync. Check it out!
Update (2020-09-14): See also: Antoine van der Lee.