Even though Core Data is pretty performant, some operations can take a while to complete. When such an operation is performed on the main thread, it blocks the main thread. The result is an unresponsive user interface, which you want to avoid at any cost.
A popular feature of Samsara is its statistics summary. It gives the user an overview of their yoga and meditation sessions. To populate this summary, the application needs to carry out a series of fetch requests.
This is not an issue if the user just started using the application and the persistent store is still small and manageable. But some people have been using Samsara for several years, resulting in pretty sizable databases.
SQLite is incredibly fast and I doubt that any of Samsara's users is going to run into performance issues. Nevertheless, it is a good idea to be prepared. There are several solutions to solve this problem. The solution I would like to discuss in this tutorial is asynchronous fetching using operations.
How Does It Work?
The idea is simple. Each fetch request is performed by an NSOperation
instance. The operation uses a private managed object context to perform the fetch request in the background and it notifies a delegate when the results are ready to be displayed to the user.
The user can use the application while the data is being fetched. The user interface is updated incrementally, that is, whenever an operation is completed, the user interface is updated with the newly fetched data.
Building a Foundation
I have chosen to create a separate subclass for every fetch request. This keeps the class focused and easy to understand. Every subclass inherits from an NSOperation
subclass that contains most of the logic for creating and preparing the operation for its task. Let me show you what that superclass looks like.
import UIKit
import CoreData
protocol CoreDataOperationDelegate {
func operation(operation: CoreDataOperation, didCompleteWithResult: [String: AnyObject])
}
class CoreDataOperation: NSOperation {
let delegate: CoreDataOperationDelegate
let privateManagedObjectContext: NSManagedObjectContext
var identifier = 0
var result = [String: AnyObject]()
// MARK: - Initialization
init(delegate: CoreDataOperationDelegate, managedObjectContext: NSManagedObjectContext) {
// Set Delegate
self.delegate = delegate
// Initialize Managed Object Context
privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
// Configure Managed Object Context
privateManagedObjectContext.persistentStoreCoordinator = managedObjectContext.persistentStoreCoordinator
super.init()
}
// MARK: - Perform Fetch Request
func performFetchRequest() {}
// MARK: - Overrides
override func main() {
performFetchRequest()
dispatch_async(dispatch_get_main_queue()) {
// Notify Delegate
self.delegate.operation(self, didCompleteWithResult: self.result)
}
}
}
The idea is straightforward. We declare a delegate protocol with a single method. This method is invoked when the operation completes. The object interested in the result of the operation's fetch request is required to implement the method of this protocol.
protocol CoreDataOperationDelegate {
func operation(operation: CoreDataOperation, didCompleteWithResult: [String: AnyObject])
}
The CoreDataOperation
class inherits from NSOperation
. It has two constant and two variable properties. The constant properties are the delegate object and the private managed object context that is used to preform the fetch request. This is a very important aspect of the CoreDataOperation
class. We need to use a private managed object context to perform the fetch request on a background queue.
The variable properties are an identifier of type Int
and a dictionary of type [String: AnyObject]
to store the result or error of the fetch request in. The identifier is used to identify an operation in the operation queue. This becomes clear in a moment.
class CoreDataOperation: NSOperation {
let delegate: CoreDataOperationDelegate
let privateManagedObjectContext: NSManagedObjectContext
var identifier = 0
var result = [String: AnyObject]()
...
}
The initializer is pretty simple. It accepts two arguments, the delegate object and the managed object context that is used to create the private managed object context. Setting up the private managed object context is straightforward. We set the concurrency type to PrivateQueueConcurrencyType
and use the managed object context to obtain a reference to the persistent store coordinator.
// MARK: - Initialization
init(delegate: CoreDataOperationDelegate, managedObjectContext: NSManagedObjectContext) {
// Set Delegate
self.delegate = delegate
// Initialize Managed Object Context
privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
// Configure Managed Object Context
privateManagedObjectContext.persistentStoreCoordinator = managedObjectContext.persistentStoreCoordinator
super.init()
}
Every subclass of the CoreDataOperation
class needs to override the performFetchRequest()
method. In this method, the operation fetches the data it is interested in from the persistent store. The implementation is empty in the CoreDataOperation
class.
// MARK: - Perform Fetch Request
func performFetchRequest() {}
The most interesting method of the CoreDataOperation
class is the main()
method. In this method, we perform the fetch request and notify the delegate of the result of the operation. The delegate object is notified on the main thread. This is not essential, but it is convenient for anyone using the CoreDataOperation
class.
// MARK: - Overrides
override func main() {
performFetchRequest()
dispatch_async(dispatch_get_main_queue()) {
// Notify Delegate
self.delegate.operation(self, didCompleteWithResult: self.result)
}
}
Performing a Fetch Request
With the CoreDataOperation
class ready to use, it is time to create a subclass that fetches data from the persistent store. The implementation can be as simple or as complex as you like. This is what one of the subclasses looks like. The operation fetches every session of the user and calculates the average session time.
import UIKit
class AverageTimeInSessionOperation: CoreDataOperation {
override func performFetchRequest() {
if let sessions = Session.findAllInManagedObjectContext(privateManagedObjectContext) as? [Session] {
// Helprs
var average: Double = 0.0
var timeInSession: Double = 0.0
let numberOfSessions = sessions.count
if numberOfSessions > 0 {
for session in sessions {
timeInSession += session.duration.doubleValue
}
// Average Time In Session
average = (timeInSession / Double(numberOfSessions));
}
// Update Result
result["result"] = NSNumber(double: average)
}
}
}
The CoreDataOperation
subclass is appropriately named AverageTimeInSessionOperation
. The class only overrides the performFetchRequest()
method. In this method, we fetch every session in the persistent store and calculate the total time the user has spent meditating. That number is divided by the number of sessions.
We store the result of the operation in the result
dictionary. The reason this object is a dictionary is simple. The code was originally written in Objective-C and I have ported it to Swift for this tutorial. It may be more elegant to use a tuple, for example. Some operations may return multiple values and it is also convenient for storing errors in. You could also pass an optional error object as an argument to the delegate method we defined earlier. That is entirely up to you.
Scheduling Operations
Let me end this tutorial by showing you how to schedule a CoreDataOperation
operation. We instantiate an instance of the AverageTimeInSessionOperation
class, passing in a delegate and a managed object context. We set the operation's identifier and add it to an operation queue. That is it.
// Initialize Operation
let operation = AverageTimeInSessionOperation(delegate: self, managedObjectContext: managedObjectContext)
// Configure Operation
operation.identifier = 1
// Add Operation to Operation Queue
operationQueue.addOperation(operation)
In the delegate method of the CoreDataOperationDelegate
protocol, we can process the result and update the user interface.
func operation(operation: CoreDataOperation, didCompleteWithResult: [String: AnyObject]) {
// Update User Interface
...
}
Exploring Operations
The NSOperation
class may seem daunting and obscure at first, but it is certainly a class worth exploring. You can take your first steps with operations by using NSBlockOperation
, a concrete NSOperation
subclass that accepts and executes a block or closure. This is a good starting point to become familiar with operations, operation queues, and asynchronous programming.
Questions? Leave them in the comments below or reach out to me on Twitter.