Structuring CoreData for Efficiency and Ease: Part 3

Bringing it all together: Implementation

Ritwik P
6 min readJan 18, 2023
Photo by Arnold Francisca on Unsplash

So, in Part 1 and Part 2 we created a CoreData implementation that does not require dealing with the pesky contexts and NSssss (For those of you just joining … NSssss is NSFetchRequest, NSFetchRequestResult, NSPredicate and all the rest NSsssss).

All that is worth shit, if we cannot implement our creation.

For understanding the implementation, we will first visit the autogenerated code from Xcode that utilises @FetchResult state and go into the details of when it should be used and when its usage will cause unknown errors in the application.

Let’s begin!

This is the code that Xcode automatically generates for us.

struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext

@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>

var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
#endif
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}

private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()

do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}

private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)

do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}

private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

The @Environment wrapper is designed there to work with SwiftUI’s pre-defined keys — In this case the managedObjectContext.

Once the environment is read for managedObjectCotext, @FetchRequest wrapper comes to play. @FetchRequest creates a fetch result just like the NSFetchRequest, however, it is, loads, more elegant about it.

And then is defined the storage variable, in this case, items, which acts as storage for FetchedResults for Item — FetchedResult<Item>.

In all a very elegant representation. Just with one problem. It all works well till the data being fetched isn’t being modified by anything else except the view in question.

If the application being designed, like the one designed by us as an example, is extremely simple, with no parallel processing elsewhere, no multithreading by other agents of the application, no complicated data modification — basically everything that a good application is supposed to implement — not to mention the inherent logic behind the application … If that is the case, use the auto-generated implementation of Xcode without a second thought.

However, if your application, like most applications, deals with data stored and data retrieved concurrently and asynchronously: Read on!

For implementing our own brand of database using CoreData we begin by defining a storage variable. In our case an array of Item struct.

struct ContentView: View {
@State private var items = [Item]()
var body: some View {
NavigationView {
List {
}
}
}
}

When the application runs, the very first thing that we need in order to display the data inside the application is to read the data from CoreData: Hence,

struct ContentView: View {
@State private var items = [Item]()
var body: some View {
NavigationView {
List {
}
.onAppear {
Task {
let query = Query()
.entityName(Model.Entities.item)
items = try! await Item.read(with: query)
}
}
}
}
}

The keyword Task is required, as we have created a database capable of async operations. In order to avoid having to use Task, you may create synchronous mirrors of the asynchronous functions used in the example application.

Now that we have the data, we need to display the current data in the stack: Hence,

struct ContentView: View {
@State private var items = [Item]()
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.name) with category \(item.ofCategory)")
} label: {
Text(item.name)
}
}
.onDelete(perform: deleteItems)
}
.onAppear {
Task {
let query = Query()
.entityName(Model.Entities.item)
items = try! await Item.read(with: query)
}
}
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
#endif
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
}

This is basically the same display code that was auto-generated by Xcode.

Now we need the addItems and deleteItems functionality to complete the example.

The original addItems supplied by Xcode is,

private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()

do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}

The code, as you maybe aware, first created an Item in the viewContext, declared as an @Environment state of the application.

Thereafter, since the app only creates the id attribute which is a Date, a new item is declared and modified. After modification the context is saved back to CoreData.

Our implementation shall be a bit different:

private func addItem() {
withAnimation {

let newItem = Item(id: UUID(version: .v3, name: "\(Date().timeIntervalSince1970)", nameSpace: .dns),
name: "\(Date())",
ofCategory: UUID(version: .v3, name: "\(Date().timeIntervalSince1970)", nameSpace: .dns))

Task {
do {
try await newItem.create()
} catch {
print(error)
}
}

items.append(newItem)
}
}

Do not worry about the UUID() extension used in the code. We begin, by declaring a newItem and then calling our create() function on the Item to save it to CoreData. Once the save is complete we append our new Item to the items @State for display on the screen. We could, if we wanted very simply re-read the entire stack using

let query = Query()
.entityName(Model.Entities.item)
items = try! await Item.read(with: query)

Or with a bit of modification to our Item struct

items = try! await Item.readAll()

But for the sake of this implementation, both are not necessary.

For deleting, the auto-generated code goes,

private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)

do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}

Which for us becomes:

private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach { item in
Task {
do {
try await item.delete()
items.remove(atOffsets: offsets)
} catch {
print(error)
}
}
}
}
}

Again, we could re-read the stack. But it’s unnecessary in our case.

Congratulations! We made it!

There are so many ways in which this basic implementation can be made better to suit your needs. But each application, has different needs and hence the code will evolve based on those needs.

I believe, we have come a long way from the verbose and complicated implementation provided by Apple. We have also made sure that all contexts are background and async assuring us of speedy animation and no lag which CoreData does what its does best — Act like a Database! 😝

Till the next Series.

Happy Coding!

--

--

Ritwik P

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