Mastodon

Storing Metadata in Core Data

Persistence frameworks for iOS seem to go from 0 to 100 real quick. Need to store a few booleans and maybe a string? UserDefaults will do just fine! How about a few tens of model instances? Well, you could either serialize it to a plist, à la UserDefaults, or here’s an entire object graph and persistence framework backed by an SQL data store and oh by the way, it’s one of the most complex Cocoa frameworks and it’s called Core Data! Don’t get me wrong, I really warmed up to Core Data after I found out how difficult it is to serialize a tree with just Swift’s Codable protocol, however defining full-blown entities for certain trivial data makes about as much sense as a nuclear hand grenade, especially when that data is metadata related to our Core Data store, such as the timestamp of the last update or the latest commit ID. Keeping this data in a store alongside the user’s preferred theme or units of measurement feels wrong, as it fragments our model layer. Luckily, Core Data comes with a baked-in solution in the form of the metadata dictionary. Each NSPersistentStore object contains a metadata property which can be read from with the NSPersistentStoreCoordinator instance method metadata(for:).

func lastUpdatedOn(_ persistentContainer: NSPersistentContainer) -> Date? {
let coordinator = persistentContainer.persistentStoreCoordinator
guard let store = coordinator.persistentStores.first else {
fatalError("Unable to retrieve persistent store")
}
let metadata = coordinator.metadata(for: store)
guard let lastUpdated: Date = metadata["lastUpdated"] as? Date else {
return nil
}
return lastUpdated
}

Note, if you’re using NSPersistentContainer, you can spare yourself the optional chaining and access your persistent store through it instead of through an NSManagedObjectContext instance.

To write metadata, use the setMetadata(_:for:) method on your store’s NSPersistentStoreCoordinator.

let coordinator = persistentContainer.persistentStoreCoordinator
guard let store = coordinator.persistentStores.first else {
fatalError("Unable to retrieve persistent store")
}
coordinator.setMetadata(["lastUpdated": lastUpdated], for: store)

After writing metadata, you’ll need to save the managed object context referring to the store’s coordinator. One possible Good Enough™ solution is to perform the metadata updates and saves in a completion handler after any transactions.

func fetchUpdates(completion: { context in
guard let coordinator = context.persistentStoreCoordinator else {
fatalError("Unable to retrieve persistent store coordinator")
}
guard let store = coordinator.persistentStores.first else {
fatalError("Unable to retrieve persistent store")
}
coordinator.setMetadata(["lastUpdated": Date.now()], for: store)
context.save()
})

An added bonus of using Core Data metadata over other persistence methods is that because it is a property of NSPersistentStore, it follows its associated data wherever the store goes! This is particularly useful if you ever need to seed or move a persistent store.

Additional Resources