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 *fetchError = 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.
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.