Using Core Data to Improve User Experience

photo-1427751840561-9852520f8ce8-248415-edited.jpeg

How can you update and display large amounts of data to a user while not ruining user experience?  There are many options, but a relational database is usually the best solution to this particular problem.  I have implemented SQL databases before, both for Android and server side implementations, and I was recently tasked with implementing a database using Core Data for an iOS app to solve an issue like this.  This shouldn’t be a problem, right?  I mean, it’s just a SQLite database at its core.  

As it turns out, I was both right and wrong.  First off, Core Data isn’t necessarily SQLite at it’s heart.  It can and probably will be, but that’s not that big a deal.  You don’t write SQL statements when you use Core Data.  The way Core Data stores its data could also be XML, Binary, or In-memory.  But after looking at all the options, I was right that SQLite wasn’t going to be a problem.  In reality, Core Data is a very high level way of looking at storing data for iOS or OS X applications.  Think object-oriented database management.

Normally, when building an integrated database with an app, there is a certain level of overhead required.  Some tables only exist to establish relationships, constants are used to maintain proper naming conventions, and helper objects are required to handle the CRUD (Create, Read, Update, and Delete) statements. Luckily, Core Data includes all this overhead in the framework, so I can focus on top level concepts like giving my users the best experience possible when dealing with large data sets.  I, like many others, fall into the trap of thinking computers are so fast that saving and retrieving data is so close to instantaneous, it might as well be.  In reality it does take time to execute code and retrieve data, and when presented with thousands or hundreds of thousands of changes, devices will need time to process the data.  Using Core Data’s high level methods, I can handle this in an intelligent way.  

In order to take full advantage of Core Data there are a few concepts that need to be understood.  Contexts, for example, are a powerful concept inherent in Core Data.  Changes to a Core Data database must first be made to a context.   Fundamentally, a context is a copy of the database stored in memory or a copy of another context.  Think of a context as a scratchpad database.  I can make changes to a context, and it won’t affect the database stored in memory until I tell the context to save itself.  Applying my changes will update either the database stored in memory or the originating context.  Confused yet?  Think of it like a mini Subversion or Git repository.  Nothing gets fully committed to the main repository until you push and merge your changes.

Changes applied to a context only occur on one of two pre-managed queues: NSMainQueueConcurrencyType and NSPrivateQueueConcurrencyType.  NSMainQueueConcurrencyType operates on the main queue of your app and will block your UI thread if given a large number of changes.  NSPrivateQueueConcurrencyType operates on a private thread and will not affect the UI thread no matter how many changes you throw at it.

screen_shot_2016-03-05_at_1.16.42_pm.png

With all that in mind, here is the diagram of the Core Data database I chose to implement in my recent project. 

I often have to make lots of changes at a time like downloading large datasets, batch changes, etc.  Therefore, a private queue makes the most sense for these changes.  This is the updateContext object as seen in the above diagram.  I present changes to the user immediately through tables using a NSFetchedResultsController.  Apple has a great tutorial on how to use a NSFetchedResultsController with sample code in Obj-C and Swift.  In order to do this, I needed a context on the main thread, hence the mainContext object represented in the diagram.  This context is only used for viewing the database.  No changes by the user or network calls are applied directly to this context.  To the user interface, this is the only window to the database, so in essence it is the app’s sole source of truth.  Finally, writing data to memory takes time.  It’s bad practice to hold up the UI for any reason, so I have a context on a private queue whose only purpose is to save data to memory, i.e. the persistContext object.

This is the flow that occurs when a change is made, like when I get data from the Event Farm API.  The network request comes back with data.  I take this data and create, update, or delete objects on the updateContext using a private thread.  When this finishes, I call updateContext.save().  This then merges the changes I made to the updateContext with the mainContext.  Merging changes with the mainContext must be done on the UI thread.  This could cause the UI thread to block if there are a lot of changes to merge.  The key is to try to do this in short bursts whenever possible.  As soon as the mainContext has changes, the NSFetchedResultsController sees these changes and updates the table visible to the user.  If these changes are something to save in memory, I call mainContext.save().  When that completes, I immediately call persistContext.save().  Now the changes are fully saved in memory.

Here’s how I set up my contexts using Swift:

// This resource is the same name as your xcdatamodeld contained in your project.
guard let modelURL = NSBundle.mainBundle().URLForResource("Event_Check_In", withExtension:"momd") else {
    fatalError("Error loading model from bundle")
}
        
// The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
guard let mom = NSManagedObjectModel(contentsOfURL: modelURL) else {
    fatalError("Error initializing mom from: (modelURL)")
}

let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)

self.persistContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
self.persistContext.persistentStoreCoordinator = psc

self.mainContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
self.mainContext.parentContext = self.persistContext

self.updateContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
self.updateContext.parentContext = self.mainContext

And this is how I save changes:

func save(completionHandler: (Void -> Void)?) {
    self.updateContext.performBlock({ () -> Void in
        if self.updateContext.hasChanges {
            print("updateContext has changed. Saving...")
            do {
                try self.updateContext.save()
            }
            catch {
                print("Unable to save updateContext: (error)")
            }
            
            self.mainContext.performBlock { () -> Void in
                if !self.mainContext.hasChanges {
                    // Nothing to save
                    completionHandler?()
                    return
                }
                
                do {
                    print("Saving to mainContext")
                    try self.mainContext.save()
                }
                catch {
                    print("Failed to save on main context: (error)")
                    completionHandler?()
                    return
                }
                
                self.persistContext.performBlock({ () -> Void in
                    
                    if !self.persistContext.hasChanges {
                        // Nothing to save
                        completionHandler?()
                        return
                    }
                    
                    print("Saving to persistant context.")
                    
                    do {
                        try self.persistContext.save()
                    }
                    catch {
                        print("Failed to save on private context: (error)")
                        completionHandler?()
                        return
                    }
                    
                    // Complete Success!!!
                    completionHandler?()
                })
            }
        }
        else {
            
            print("Nothing has changed with updateContext.")
            
            completionHandler?()
        }
    })
}

Using three contexts may seem a little overkill when it is possible to just use one.  However, using this architecture, most of the processing takes place off the main thread.  The UI thread will remain responsive, and I’ve found that this makes a huge difference in user experience. It shows the power of Core Data with the basic understanding of just a few concepts. At its roots, It’s a simple object-oriented way of looking at database management.