Structuring CoreData for Efficiency and Ease: Part 1

Part 1: First things first: Separate Logic from Crap!

Ritwik P
9 min readJan 12, 2023
Photo by Kevin Ku on Unsplash

Yes! Yes, I know, I know … Everyone kindly calm down!

“Core Data is not a database. Core Data is a framework for managing an object graph. An object graph is nothing more than a collection of interconnected objects. The framework excels at managing complex object graphs”

“CoreData is a persistent object graph not a database”

“CoreData is so much more than a database. Don’t be a noob!”

Yes! I get it! But, I do not care. I think those of you reading, not those ranting, want a simple database that doesn’t take time to implement, manages the faulting on its own, and is easy to implement on device and in the cloud — That is exactly what CoreData does.

So, not spending too much time defending myself. Moving on.

Problems with using Coredata

  • Used incorrectly it is slower than NoSQL databases. So if speed is of the essence you should probably look elsewhere. However, I have always found it to be plenty fast and then some more. (Once had a multi GB Database with a complex model and never felt it slow down!).

There will always be scary cats driving at 20 Mph, wanting to own a Ferrari!

  • Problem of Thread Concurrency: Accessing data and making changes can get really fucked up when using multithreading application. To be fair though, it is fucked up with some NoSQL databases too: Prime Example: Realm!
  • Passing Coredata Objects: That is actually more of a problem regarding Classes than Coredata. But its is important nevertheless!
  • Overheads: Specifically the overhead caused by relationships.

What else? I am sure there are other problems. But I am no “expert”, so just stick to these for now!

How I recommend using CoreData

Boilerplating

Follow the basic steps. > New Project > Platform > Name > Select Using Coredata > (Using CloudKit) > Create.

For explaining how I recommend using coredata, I will use the example project that is by default created by Xcode, you know, just to keep things simple and abstaining from forcing you to download git code.

This is what we have from Xcode as our Model.

Default XCode
Default XCode

And this is auto generated code:

struct PersistenceController {
static let shared = PersistenceController()

static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()

let container: NSPersistentCloudKitContainer

init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "CoreDataStructuring")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}

Let us make a few changes. The model we shall use contains 2 Entities. Category and Item and a relationship One to Many between Category -> Item which we shall implement on our own, through code, rather than inside the CoreData model for speed and efficiency. For more understanding of overheads in coredata, I would recommend reading “THE HIDDEN COST OF CORE DATA MANAGED RELATIONSHIPS” by the Adidas Runtastic Team.

After making the basic changes and cleaning up auto generated code for “Item” by XCode this is what the boilerplate of our Coredata Implementation looks like:

Summarising, There are two Entities — Category and Item — each containing two Attributes — id and name — with a relationship (Not created through CoreData) of type Category(One) -> Item(Many) by an extra attribute in Item: ofCategory.

First things first

In having used coredata for quite a few years, having moved to other NoSQL databases and moving back to Coredata, I came to understand the things that make usage of Coredata complicated or in simpler words NO FUN AT ALL!

The first is verboseness of the code. I mean: come on! Just look at the work required to create a simple read.

let context = PersistenceController.shared.container.newBackgroundContext()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Item")
fetchRequest.predicate = NSPredicate(format: "id == %@", "id")

do {
let items = try context.fetch(fetchRequeast) as! [Item]
} catch {
fatalError()
}

Not to mention that the entirety of the implementation is now inside a do-try-catch block, completely cutoff from the rest of the code.

In order to simplify and increase reusability create two structs mirroring our Coredata Model: Item(CDItem) and Category(CDCategory)

struct Item: Identifiable, Hashable {
var id: UUID
var name: String
var ofCategory: UUID
}

struct Category: Identifiable, Hashable {
var id: UUID
var name: String
}

Simple! Isn’t it! Well … wait for it!

Now we create an implementation for our Coredata database by creating a Protocol: DatabaseProtocol and conform our new structs to it.

protocol DatabaseProtocol {
associatedtype T: Hashable

func create() async throws
func update() async throws
static func read(with predicate: NSPredicate) async throws -> [T]
func delete() async throws

static func create(batch: Set<T>) async throws
static func update(batch: Set<T>) async throws

static func delete(predicate: NSPredicate) async throws
}

Conforming Item to DatabaseProtocol

extension Item: DatabaseProtocol {

/// Creates a new entry in CoreData
///
/// - throws DatabaseError
func create() async throws {
let context = PersistenceController.shared.container.newBackgroundContext()

let newItem = CDItem(context: context)
newItem.id = self.id
newItem.name = self.name
newItem.ofCategory = self.ofCategory

do {
try context.performAndWait {
try context.save()
}
} catch {
throw DatabaseError.createError
}
}

/// Update entry in CoreData
///
/// - throws DatabaseError
func update() async throws {
let context = PersistenceController.shared.container.newBackgroundContext()

let fetchRequest = NSFetchRequest<CDItem>(entityName: "CDItem")
fetchRequest.predicate = NSPredicate(format: "id == %@", self.id as CVarArg)

do {
let items = try context.fetch(fetchRequest)
if !items.isEmpty {
let itemToUpdate = items[0]
itemToUpdate.setValue(self.id, forKey: "id")
itemToUpdate.setValue(self.name, forKey: "name")
itemToUpdate.setValue(self.ofCategory, forKey: "ofCategory")

do {
try context.performAndWait {
try context.save()
}
} catch {
throw DatabaseError.updateError
}
}
} catch {
throw DatabaseError.readError
}
}

/// Read from CoreData
///
/// - parameter predicate: Filter as NSPredicate
///
/// - throws DatabaseError
/// - returns: [Item]
static func read(with predicate: NSPredicate) async throws -> [Item] {
let context = PersistenceController.shared.container.newBackgroundContext()

let fetchRequest = NSFetchRequest<CDItem>(entityName: "CDItem")
fetchRequest.predicate = predicate

do {
let cDItems = try context.fetch(fetchRequest)
let items = cDItems.map { cdItem in
return Item(id: cdItem.id!, name: cdItem.name!, ofCategory: cdItem.ofCategory!)
}

return items
} catch {
throw DatabaseError.readError
}
}


/// Delete from CoreData
///
/// - throws DatabaseError
func delete() async throws {
let context = PersistenceController.shared.container.newBackgroundContext()

let fetchRequest = NSFetchRequest<CDItem>(entityName: "CDItem")
fetchRequest.predicate = NSPredicate(format: "id == %@", self.id as CVarArg)

do {
let items = try context.fetch(fetchRequest)
if !items.isEmpty {
let itemToDelete = items[0]
context.performAndWait {
context.delete(itemToDelete)
}
}
} catch {
throw DatabaseError.readError
}
}


/// Create a batch in Coredata
///
/// - parameter batch: Set of Items
///
/// - throws: DatabaseError
static func create(batch: Set<Item>) async throws {
let context = PersistenceController.shared.container.newBackgroundContext()

for item in batch {
let newItem = CDItem(context: context)
newItem.id = item.id
newItem.name = item.name
newItem.ofCategory = item.ofCategory
}

do {
try context.performAndWait {
try context.save()
}
} catch {
throw DatabaseError.createError
}
}


/// Updates a batch in Coredata
///
/// - parameter batch: Set of Items
///
/// - throws: DatabaseError
static func update(batch: Set<Item>) async throws {
let context = PersistenceController.shared.container.newBackgroundContext()

for item in batch {
do {
try await item.update()
} catch {
throw DatabaseError.updateError
}
}

do {
try context.performAndWait {
try context.save()
}
} catch {
throw DatabaseError.updateError
}
}


/// Delete a batch in Coredata
///
/// - parameter predicate: Predicate for Delete
///
/// - throws: DatabaseError
static func delete(predicate: NSPredicate) async throws {
let context = PersistenceController.shared.container.newBackgroundContext()

let fetchRequest = NSFetchRequest<CDItem>(entityName: "CDItem")
fetchRequest.predicate = predicate

do {
let items = try context.fetch(fetchRequest)
if !items.isEmpty {
for item in items {
try context.performAndWait {
context.delete(item)
try context.save()
}
}
}
} catch {
throw DatabaseError.readError
}
}
}

Similarly coforming Category

extension Category: DatabaseProtocol {

/// Creates a new entry in CoreData
///
/// - throws DatabaseError
func create() async throws {
let context = PersistenceController.shared.container.newBackgroundContext()

let newCategory = CDCategory(context: context)
newCategory.id = self.id
newCategory.name = self.name

do {
try context.performAndWait {
try context.save()
}
} catch {
throw DatabaseError.createError
}
}

/// Update entry in CoreData
///
/// - throws DatabaseError
func update() async throws {
let context = PersistenceController.shared.container.newBackgroundContext()

let fetchRequest = NSFetchRequest<CDCategory>(entityName: "CDCategory")
fetchRequest.predicate = NSPredicate(format: "id == %@", self.id as CVarArg)

do {
let categories = try context.fetch(fetchRequest)
if !categories.isEmpty {
let categoryToUpdate = categories[0]
categoryToUpdate.setValue(self.id, forKey: "id")
categoryToUpdate.setValue(self.name, forKey: "name")

do {
try context.performAndWait {
try context.save()
}
} catch {
throw DatabaseError.updateError
}
}
} catch {
throw DatabaseError.readError
}
}


/// Read from CoreData
///
/// - parameter predicate: Filter as NSPredicate
///
/// - throws DatabaseError
/// - returns: [Category]
static func read(with predicate: NSPredicate) async throws -> [Category] {
let context = PersistenceController.shared.container.newBackgroundContext()

let fetchRequest = NSFetchRequest<CDCategory>(entityName: "CDCategory")
fetchRequest.predicate = predicate

do {
let CDCategorys = try context.fetch(fetchRequest)
let categories = CDCategorys.map { CDCategory in
return Category(id: CDCategory.id!, name: CDCategory.name!)
}

return categories
} catch {
throw DatabaseError.readError
}
}


/// Delete from CoreData
///
/// - throws DatabaseError
func delete() async throws {
let context = PersistenceController.shared.container.newBackgroundContext()

let fetchRequest = NSFetchRequest<CDCategory>(entityName: "CDCategory")
fetchRequest.predicate = NSPredicate(format: "id == %@", self.id as CVarArg)

do {
let categories = try context.fetch(fetchRequest)
if !categories.isEmpty {
let categoryToDelete = categories[0]
context.performAndWait {
context.delete(categoryToDelete)
}
}
} catch {
throw DatabaseError.readError
}
}

/// Create a batch in Coredata
///
/// - parameter batch: Set of Categories
///
/// - throws: DatabaseError
static func create(batch: Set<Category>) async throws {
let context = PersistenceController.shared.container.newBackgroundContext()

for category in batch {
let newCategory = CDCategory(context: context)
newCategory.id = category.id
newCategory.name = category.name
}

do {
try context.performAndWait {
try context.save()
}
} catch {
throw DatabaseError.createError
}
}


/// Updates a batch in Coredata
///
/// - parameter batch: Set of Categories
///
/// - throws: DatabaseError
static func update(batch: Set<Category>) async throws {
let context = PersistenceController.shared.container.newBackgroundContext()

for category in batch {
do {
try await category.update()
} catch {
throw DatabaseError.updateError
}
}

do {
try context.performAndWait {
try context.save()
}
} catch {
throw DatabaseError.updateError
}
}


/// Delete a batch in Coredata
///
/// - parameter predicate: Predicate for Delete
///
/// - throws: DatabaseError
static func delete(predicate: NSPredicate) async throws {
let context = PersistenceController.shared.container.newBackgroundContext()

let fetchRequest = NSFetchRequest<CDCategory>(entityName: "CDCategory")
fetchRequest.predicate = predicate

do {
let categories = try context.fetch(fetchRequest)
if !categories.isEmpty {
for category in categories {
try context.performAndWait {
context.delete(category)
try context.save()
}
}
}
} catch {
throw DatabaseError.readError
}
}
}

Phew!!!!!!

PHEW!!!!

But why? Why O why make an automated implementation so damn complicated?

Well … Remember the fetchRequest for performing a simple read?

Now that request (Without worrying in the slightest about Concurrency issues, Threading or Unknown Crashes) becomes

let predicate = NSPredicate(format: "id == %@", "id")
guard let items = try? await Item.read(with: predicate) else { return }

Compared to

let context = PersistenceController.shared.container.newBackgroundContext()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Item")
fetchRequest.predicate = NSPredicate(format: "id == %@", "id")

do {
let items = try context.fetch(fetchRequeast) as! [Item]
} catch {
fatalError()
}

In essence what we have accomplished is that we have hidden away the code for accessing data inside Coredata into a neat little (😆) encapsulation and created an interface between the object graph and the accessed data. By using structs we have created value types that can be copied around the project until it is ready to be updated to the (I know, I KNOW … CoreData is not a database) database!

We will further explore, in the next part of the series, on how to use this functionality to ease Coredata implementation without worrying about the hassle of contexts, threading or async-await operations among other things.

Till we meet again.

Happy Coding!

--

--

Ritwik P

I am an author by heart, bureaucrat by accident, coder by choice and a human by chance ;)