Core data


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 using shift + cmd + g. See CDHelper.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:

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, and ENDSWITH 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