iOS 8: Core Data and Batch Updates

This post is part of a series called Core Data from Scratch.
Core Data from Scratch: Concurrency
iOS 8: Core Data and Asynchronous Fetching
Core Data has been around for many years on OS X and it didn't take Apple long to bring it to iOS. Even though the framework doesn't get as much attention as extensions or handoff, it continues to evolve year over year, and this year, with the release of iOS 8 and OS X Yosemite, is no different.

Apple introduced a few new features to the Core Data framework, but the most notable are batch updates and asynchronous fetching. Developers have been asking for these features for many years and Apple finally found a way to integrate them into Core Data. In this tutorial, I will show you how batch updates work and what they mean for the Core Data framework.

1. The Problem

Core Data is great at managing object graphs. Even complex object graphs with many entities and relationships aren't much of a problem for Core Data. However, Core Data does have a few weak spots and updating a large number of records is one of them.

The problem is easy to understand. Whenever you update a record, Core Data loads the record into memory, updates the record, and save the changes to the persistent store, a SQLite database for example.

If Core Data needs to update a large number of records, it needs to load every single record into memory, update the record, and send the changes to the persistent store. If the number of records is too large, iOS will simply bail out due to a lack of resources. Even though a device running OS X may have the resources to execute the request, it will be slow and consume a lot of memory.

An alternative approach is to update the records in batches, but that too takes a lot of time and resources. On iOS 7, it's the only option iOS developers have. That is no longer the case on iOS 8.

2. The Solution

On iOS 8 and OS X Yosemite, it's possible to talk directly to the persistent store and tell it what you'd like to change. This generally involves updating an attribute or deleting a number of records. Apple calls this feature batch updates.

There are a number of pitfalls to watch out for though. Core Data does a lot of things for you and you may not even realize it until you use batch updates. Validation is a good example. Because Core Data performs batch updates directly on the persistent store, such as a SQLite database, Core Data isn't able to perform any validation on the data you insert. This means that you are in charge of making sure you don't add invalid data to the persistent store.

When would you use batch updates? Apple recommends to only use this feature if the traditional approach is too resource or time intensive. If you need to mark hundreds or thousands of email messages as read, then batch updates is the best solution on iOS 8 and OS X Yosemite.

3. How Does It Work?

To illustrate how batch updates work, I suggest we revisit Done, a simple Core Data application that manages a to-do list. We'll add a button to the navigation bar that marks every item in the list as done.

Step 1: Projet Setup
Download or clone the project from GitHub and open it in Xcode 6. Run the application in the iOS Simulator and add a few to-do items.

Step 2: Create Bar Button Item
Open TSPViewController.m and declare a property, checkAllButton, of type UIBarButtonItem in the private class extension at the top.

@interface TSPViewController () <NSFetchedResultsControllerDelegate>

@property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController;

@property (strong, nonatomic) UIBarButtonItem *checkAllButton;

@property (strong, nonatomic) NSIndexPath *selection;

@end
Initialize the bar button item in the viewDidLoad method of the TSPViewController class and set it as the left bar button item of the navigation item.

// Initialize Check All Button
self.checkAllButton = [[UIBarButtonItem alloc] initWithTitle:@"Check All" style:UIBarButtonItemStyleBordered target:self action:@selector(checkAll:)];
    
// Configure Navigation Item
self.navigationItem.leftBarButtonItem = self.checkAllButton;
Step 3: Implement checkAll: Method
The checkAll: method is fairly easy, but there are a few caveats to watch out for. Take a look at its implementation below.

- (void)checkAll:(id)sender {
    // Create Entity Description
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"TSPItem" inManagedObjectContext:self.managedObjectContext];
    
    // Initialize Batch Update Request
    NSBatchUpdateRequest *batchUpdateRequest = [[NSBatchUpdateRequest alloc] initWithEntity:entityDescription];
    
    // Configure Batch Update Request
    [batchUpdateRequest setResultType:NSUpdatedObjectIDsResultType];
    [batchUpdateRequest setPropertiesToUpdate:@{ @"done" : @YES }];
    
    // Execute Batch Request
    NSError *batchUpdateRequestError = nil;
    NSBatchUpdateResult *batchUpdateResult = (NSBatchUpdateResult *)[self.managedObjectContext executeRequest:batchUpdateRequest error:&batchUpdateRequestError];
    
    if (batchUpdateRequestError) {
        NSLog(@"Unable to execute batch update request.");
        NSLog(@"%@, %@", batchUpdateRequestError, batchUpdateRequestError.localizedDescription);
        
    } else {
        // Extract Object IDs
        NSArray *objectIDs = batchUpdateResult.result;
        
        for (NSManagedObjectID *objectID in objectIDs) {
            // Turn Managed Objects into Faults
            NSManagedObject *managedObject = [self.managedObjectContext objectWithID:objectID];
            
            if (managedObject) {
                [self.managedObjectContext refreshObject:managedObject mergeChanges:NO];
            }
        }
        
        // Perform Fetch
        NSError *fetchError = nil;
        [self.fetchedResultsController performFetch:&fetchError];
        
        if (fetchError) {
            NSLog(@"Unable to perform fetch.");
            NSLog(@"%@, %@", fetchError, fetchError.localizedDescription);
        }
    }
}
Create Batch Request

We start by creating an NSEntityDescription instance for the TSPItem entity and use it to initialize a NSBatchUpdateRequest object.

// Create Entity Description
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"TSPItem" inManagedObjectContext:self.managedObjectContext];

// Initialize Batch Update Request
NSBatchUpdateRequest *batchUpdateRequest = [[NSBatchUpdateRequest alloc] initWithEntity:entityDescription];
We set the result type of the batch update request to NSUpdatedObjectIDsResultType, which means that the result of the batch update request will be an array containing the object IDs, instances of the NSManagedObjectID class, of the records that were changed by the batch update request.

// Configure Batch Update Request
[batchUpdateRequest setResultType:NSUpdatedObjectIDsResultType];
We also populate the propertiesToUpdate property of the batch update request. For this example, we set propertiesToUpdate to an NSDictionary containing one key, @"done", with value @YES. This simply means that every TSPItem record will be set to done, which is exactly what we want.

// Configure Batch Update Request
[batchUpdateRequest setPropertiesToUpdate:@{ @"done" : @YES }];
Execute Batch Update Request

Even though batch updates bypass the managed object context, executing a batch update request is done by calling executeRequest:error: on a NSManagedObjectContext instance. The first argument is an instance of the NSPersistentStoreRequest class. To execute a batch update, we pass the batch update request we just created. This works fins since the NSBatchUpdateRequest class is a NSPersistentStoreRequest subclass. The second argument is a pointer to an NSError object.

// Execute Batch Request
NSError *batchUpdateRequestError = nil;
NSBatchUpdateResult *batchUpdateResult = (NSBatchUpdateResult *)[self.managedObjectContext executeRequest:batchUpdateRequest error:&batchUpdateRequestError];
Updating the Managed Object Context

As I mentioned earlier, batch updates bypass the managed object context. This gives batch updates their power and speed, but it also means that the managed object context isn't aware of the changes we made. To remedy this issue, we need to do two things:

turn the managed objects that were updated by the batch update into faults
tell the fetched results controller to perform a fetch to update the user interface
This is what we do in the next few lines of the checkAll: method. We first check if the batch update request was successful by checking the batchUpdateRequestError for nil. If successful, we extract the array of NSManagedObjectID instances from the NSBatchUpdateResult object.

// Extract Object IDs
NSArray *objectIDs = batchUpdateResult.result;
We then iterate over the objectIDs array and ask the managed object context for the corresponding NSManagedObject instance. If the managed object context returns a valid managed object, we turn it into a fault by invoking refreshObject:mergeChanges:, passing in the managed object as the first argument. To force the managed object into a fault, we pass NO as the second argument.

// Extract Object IDs
NSArray *objectIDs = batchUpdateResult.result;

for (NSManagedObjectID *objectID in objectIDs) {
    // Turn Managed Objects into Faults
    NSManagedObject *managedObject = [self.managedObjectContext objectWithID:objectID];
    
    if (managedObject) {
        [self.managedObjectContext refreshObject:managedObject mergeChanges:NO];
    }
}

Fetching Updated Records

The last step is to tell the fetched results controller to perform a fetch to update the user interface. If this is unsuccessful, we log the corresponding error.

// Perform Fetch
NSError *fetchEror = nil;
[self.fetchedResultsController performFetch:&fetchError];

if (fetchError) {
    NSLog(@"Unable to perform fetch.");
    NSLog(@"%@, %@", fetchError, fetchError.localizedDescription);
}
While this may seem cumbersome and fairly complex for an easy operation, keep in mind that we bypass the Core Data stack. In other words, we need to take care of some housekeeping that's usually done for us by Core Data.

Advertisement
Step 4: Build & Run
Build the project and run the application in the iOS Simulator or on a physical device. Click or tap the bar button item on the right to check every to-do item in the list.

Conclusion

Batch updates are a great addition to the Core Data framework. Not only does it answer a need developers have had for many years, it isn't difficult to implement as long as you keep a few basic rules in mind. In the next tutorial, we'll take a closer look at asynchronous fetching, another new feature of the Core Data framework.

Comments

Popular posts from this blog

How to Create a Yoga Goddess Illustration in Adobe Illustrator

How to Create an Icon Set using Adobe Photoshop

Android Essentials: Using the Contact Picker