Notes on core data
Basics:
- The Core Data database file is called
.xcdatamodeld
, which is compiled to a.momd
file on build. - An entity in Core Data is similar to a table in a relational database, as it represents a collection of related data.
- An attribute in Core Data is a piece of information attached to a particular entity. For example, an Employee entity could have attributes for the employee’s name, position, and salary.
- An NSManagedObject represents a single object stored in Core Data; you must use it to create, edit, save, and delete from your Core Data persistent store.
- Core Data can be backed by XML, binary, or SQLite stores, as well as an in-memory store.
Gotchas:
- The Core Data build flow is as follows:
.xcdatamodeld
(XML structure) ->.momd
(NSArchived model in raw data) ->.sqlite
(in the user’s document folder). - To use your own class models instead of Xcode’s autogenerated model for Core Data, follow the steps outlined in this Stack Overflow post: https://stackoverflow.com/a/40379003/5389500.
- It appears that compiling a Core Data model with CLI spm is not currently possible, as discussed in this Swift forum post: https://forums.swift.org/t/build-swiftpm-with-resources-through-cli/45696.
- Core Data persists the SQLite file in the user’s document folder. In the simulator, you can access this SQLite file by printing the
/document
folder path and then navigating to it in Finder usingshift + cmd + g
. SeeCDHelper.coreDataDBPath
for code. - The archive of
NSManagedObjectModel
produced is identical to the.mom
file Xcode generates. The keyed unarchiver and-[NSManagedObjectModel initWithContentsOfURL:]
are interchangeable. https://stackoverflow.com/a/22649763/5389500
Resources:
- A collection of free videos covering all aspects of Core Data: https://cocoacasts.com/collections/core-data-fundamentals
- A tutorial on creating a Core Data model programmatically: https://www.cocoanetics.com/2012/04/creating-a-coredata-model-in-code/
- A tutorial on setting up the Core Data stack manually without Xcode: https://cocoacasts.com/setting-up-the-core-data-stack-from-scratch and the code: https://github.com/bartjacobs/SettingUpTheCoreDataStackFromScratch
- A pretty nice API setup for spm + Core Data, but no information on Core Data file setup, etc.: https://betterprogramming.pub/core-data-in-a-swift-package-86bf759c3b6
- A tutorial on creating the model programmatically, but no information on how to persist the model: https://dmytro-anokhin.medium.com/core-data-and-swift-package-manager-6ed9ff70921a. A simplified tutorial of the same concept: https://tigi44.github.io/ios/iOS,-Swift-Core-Data-Model-in-a-Swift-Package/
- A tutorial on importing a
.sqlite
file from a bundle (still needs a.mom
file): https://www.raywenderlich.com/2935-core-data-on-ios-5-tutorial-how-to-preload-and-import-existing-data (maybe a solution would be to use SQLite + programmatic model?) - A tutorial on loading Core Data with NSCoding (could be done from Resources/file.xxx): https://adrian.schoenig.me/blog/2021/08/18/core-data-and-spm/
- Core Data seems hard to add to SPM CLI + CI. So maybe go for SQLite? https://github.com/stephencelis/SQLite.swift
- A framework example for Core Data: https://github.com/andrewcbancroft/CoreDataFrameworkExample/tree/master/Carz (won’t work in a pure package-based SPM CI/CLI setup)
- A declarative way to describe a Core Data model in code: https://dmytro-anokhin.medium.com/core-data-and-swift-package-manager-6ed9ff70921a and https://github.com/dmytro-anokhin/core-data-model-description
- Some really cool code snippets to use: https://betterprogramming.pub/core-data-building-a-custom-store-84d19f39dec4
- A basic tutorial on relations in Core Data: https://code.tutsplus.com/tutorials/core-data-and-swift-relationships-and-more-fetching–cms-25070
Pro’s:
- The Core Data API is pretty extensive.
- There is a lot of information online on how to use Core Data.
- Many people use Core Data.
- Core Data has some multithread magic built in to avoid deadlocks.
Con’s:
- The Core Data API is very complex and requires a simplified layer to work easily with.
- Apple’s documentation is old and contains a lot of Objective-C code.
- Programmatic Core Data is pretty portable, but Core Data is not as portable as SQLite, which is just one file.
- Programmatic Core Data is the solution to the clunky setup in Xcode GUI.
- It was previously thought that Core Data did not work with Swift-package-manager in CLI or CI, but programmatic Core Data works fine with CLI/CI.
Final notes:
- Programmatic Core Data makes it easier to migrate to new database versions in the future, and it is also possibly testable with a pure package-based SPM CLI/CI.
Simple example:
https://medium.com/xcblog/core-data-with-swift-4-for-beginners-1fc067cca707 Save
let appDelegate = UIApplication.shared.delegate as! AppDelegate
// We need to create a context from this container.
let context = appDelegate.persistentContainer.viewContext
// Now let’s create an entity and new user records.
let entity = NSEntityDescription.entity(forEntityName: "Users", in: context)
let newUser = NSManagedObject(entity: entity!, insertInto: context)
// At last, we need to add some data to our newly created record for each keys using
newUser.setValue("Shashikant", forKey: "username")
newUser.setValue("1234", forKey: "password")
newUser.setValue("1", forKey: "age")
//Now we have set all the values. The next step is to save them inside the Core Data
//Save the Data
//The methods for saving the context already exist in the AppDelegate but we can explicitly add this code to save the context in the Database. Note that, we have to wrap this with do try and catch block.
do {
try context.save()
} catch {
print("Failed saving")
}
Read
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Users")
// request.predicate = NSPredicate(format: "age = %@", "12")
request.returnsObjectsAsFaults = false // what does this do again? 🤔
do {
let result = try context.fetch(request)
for data in result as! [NSManagedObject] {
print(data.value(forKey: "username") as! String)
}
} catch {
print("Failed")
}
Searching with NSPredicate:
LIKE
,CONTAINS
,MATCHES
,BEGINSWITH
, andENDSWITH
you can perform a wide array of queries in Core Data with String arguments.- You can also get sorted and with a limit see: https://nspredicate.xyz/coredata
- You can also combine predicates with NSCompoundPredicate
let query = "Rob" let request: NSFetchRequest<Person> = Person.fetchRequest() request.predicate = NSPredicate(format: "name CONTAINS %@", query)
Example CRUD utility code:
/**
* ## Examples:
* self.save(name: "Alex")
*/
func save(name: String) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let managedContext = appDelegate.persistentContainer.viewContext // You can consider a managed object context as an in-memory “scratchpad” for working with managed objects.
let entity = NSEntityDescription.entity(forEntityName: "Person", in: managedContext)!
let person = NSManagedObject(entity: entity, insertInto: managedContext) // first, you insert a new managed object into a managed object context;
person.setValue(name, forKeyPath: "name") // You must spell the KVC key (name in this case) exactly as it appears in your Data Model, otherwise, your app will crash at runtime.
do {
try managedContext.save() // you “commit” the changes in your managed object context to save it to disk.
people.append(person)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
func read() {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let managedContext = appDelegate.persistentContainer.viewContext // grab a reference to its persistent container to get your hands on its NSManagedObjectContext.
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person") // fetch all Person entities.
do {
var people: [NSManagedObject] = try managedContext.fetch(fetchRequest)
people.forEach { person in let name = person.value(forKeyPath: "name"); print(name) } // Alex
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
/**
* Update
* ## Examples:
* self.update(oldName: "Alex", newName: "Edward")
*/
func update(oldName: String, newName: String) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person") // fetch all
do {
var people: [NSManagedObject] = try managedContext.fetch(fetchRequest)
guard let match: NSManagedObject = people.first(where: { $0.value(forKeyPath: "name") == oldName }) else { return }
match.setValue(newName, forKeyPath: "name")
try managedContext.save() // you “commit” the changes in your managed object context to save it to disk.
people.append(person)
} catch let error as NSError {
print("err. \(error), \(error.userInfo)")
}
}
/**
* Delete
* ## Examples:
* self.delete(name: "James")
*/
func delete(name: String) {
let appDel: AppDelegate = (UIApplication.sharedApplication().delegate as AppDelegate)
let context = self.appDel.managedObjectContext!
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person") // fetch all
guard let match: NSManagedObject = people.first(where: { $0.value(forKeyPath: "name") == name }) else { return }
context.delete(match)
do {
try context.save()
}
catch {
// Handle Error
}
}
Storing array or dict in CoreData:
-
You can store an NSArray or an NSDictionary as a transformable attribute. This will use the NSCoding to serialize the array or dictionary to an NSData attribute (and appropriately deserialize it upon access.
-
Swift 3 As we don’t have the implementation files anymore as of Swift 3, what we have to do is going to the xcdatamodeld file, select the entity and the desired attribute (in this example it is called values). Set it as transformable and its custom class to [Double]. Now use it as a normal array.
-
Converting to data is also a posibility (. Using a transformable is the preferred way.): https://stackoverflow.com/a/40101654/5389500
Creating core data model programatically:
internal var _model: NSManagedObjectModel {
let model = NSManagedObjectModel()
// Create the entity
let entity = NSEntityDescription()
entity.name = "DTCachedFile"
// Assume that there is a correct
// `CachedFile` managed object class.
entity.managedObjectClassName = String(CachedFile)
// Create the attributes
var properties = Array<NSAttributeDescription>()
let remoteURLAttribute = NSAttributeDescription()
remoteURLAttribute.name = "remoteURL"
remoteURLAttribute.attributeType = .StringAttributeType
remoteURLAttribute.optional = false
remoteURLAttribute.indexed = true // what does this do again?
properties.append(remoteURLAttribute)
let fileDataAttribute = NSAttributeDescription()
fileDataAttribute.name = "fileData"
fileDataAttribute.attributeType = .BinaryDataAttributeType
fileDataAttribute.optional = false
fileDataAttribute.allowsExternalBinaryDataStorage = true // what does this do again?
properties.append(fileDataAttribute)
let lastAccessDateAttribute = NSAttributeDescription()
lastAccessDateAttribute.name = "lastAccessDate"
lastAccessDateAttribute.attributeType = .DateAttributeType
lastAccessDateAttribute.optional = false
properties.append(lastAccessDateAttribute)
let expirationDateAttribute = NSAttributeDescription()
expirationDateAttribute.name = "expirationDate"
expirationDateAttribute.attributeType = .DateAttributeType
expirationDateAttribute.optional = false
properties.append(expirationDateAttribute)
let contentTypeAttribute = NSAttributeDescription()
contentTypeAttribute.name = "contentType"
contentTypeAttribute.attributeType = .StringAttributeType
contentTypeAttribute.optional = true
properties.append(contentTypeAttribute)
let fileSizeAttribute = NSAttributeDescription()
fileSizeAttribute.name = "fileSize"
fileSizeAttribute.attributeType = .Integer32AttributeType
fileSizeAttribute.optional = false
properties.append(fileSizeAttribute)
let entityTagIdentifierAttribute = NSAttributeDescription()
entityTagIdentifierAttribute.name = "entityTagIdentifier"
entityTagIdentifierAttribute.attributeType = .StringAttributeType
entityTagIdentifierAttribute.optional = true
properties.append(entityTagIdentifierAttribute)
// Add attributes to entity
entity.properties = properties
// Add entity to model
model.entities = [entity]
// Done :]
return model
}
Getting meta data:
Instead of using UserDefault we can use something similar with CoreData. A persistent “key, value” store that is less complex than full blow core-data sqlite store.
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
}
Setting meta data:
let coordinator = persistentContainer.persistentStoreCoordinator
guard let store = coordinator.persistentStores.first else { fatalError("Unable to retrieve persistent store") }
coordinator.setMetadata(["lastUpdated": Date.now()], for: store)
let context = ...
context.save()
Relation delete rules:
- Deny → If there is at least one object at the relationship destination (employees), do not delete the source object (department).
- Nullify → Remove the relationship between the objects, but do not delete either object.
- Cascade → Delete the objects at the destination of the relationship when you delete the source.
- No Action → Do nothing to the object at the destination of the relationship.
No Action rule might be of use, because if you use it, it is possible to leave the object graph in an inconsistent state (employees having a relationship to a deleted department).
Gotchas:
- Sort descriptors are great and easy to use, but predicates are what really makes fetching powerful in Core Data. Sort descriptors tell Core Data how the records need to be sorted.
- Predicates tell Core Data what records you’re interested in.
Migration resources:
- https://getlotus.app/9-do-not-disturb-and-sqlite-data-storage
- https://www.raywenderlich.com/books/core-data-by-tutorials/v7.0/chapters/6-versioning-migration
- https://www.raywenderlich.com/7585-lightweight-migrations-in-core-data-tutorial not obfuscated
- https://cocoacasts.com/migrating-a-data-model-with-core-data
- https://medium.com/@maddy.lucky4u/swift-4-core-data-part-5-core-data-migration-3fc32483a5f2
- https://github.com/JohnCoates/Slate/blob/master/Source/Database/Data%20Model/Migrating/DataMigrator.swift
- https://github.com/JohnCoates/Slate/blob/master/Source/Database/Data%20Model/Migrating/SingleMigration.swift
- For version extractor code: https://github.com/JohnCoates/Slate/blob/master/Source/Database/Data%20Model/Metadata/DataModelMetadata.swift
- json -> struct -> coredata https://github.com/JohnCoates/Slate/tree/master/Source/Database/DSL
- moving persistentstore aka sqlite file to new location: https://useyourloaf.com/blog/moving-core-data-files/
- Make coredata model delcelrative: https://github.com/dmytro-anokhin/core-data-model-description/tree/master/Sources/CoreDataModelDescription
- Migrate file: https://useyourloaf.com/blog/moving-core-data-files/
- Has alot of good info and code: (payed) https://www.kodeco.com/books/core-data-by-tutorials/v8.0/chapters/6-versioning-migration
- ✨ A simple swift package that supports background context: https://github.com/avdyushin/CoreDataStorage