This post is part of a series called Core Data from Scratch.
iOS 8: Core Data and Batch Updates
In the previous article about iOS 8 and Core Data, we discussed batch updates. Batch updates aren't the only new API in town. As of iOS 8 and OS X Yosemite, it's possible to asynchronously fetch data. In this tutorial, we'll take a closer look at how to implement asynchronous fetching and in what situations your application can benefit from this new API.
1. The Problem
Like batch updates, asynchronous fetching has been on the wish list of many developers for quite some time. Fetch requests can be complex, taking a non-trivial amount of time to complete. During that time the fetch request blocks the thread it's running on and, as a result, blocks access to the managed object context executing the fetch request. The problem is simple to understand, but what does Apple's solution look like.
2. The Solution
Apple's answer to this problem is asynchronous fetching. An asynchronous fetch request runs in the background. This means that it doesn't block other tasks while it's being executed, such as updating the user interface on the main thread.
Asynchronous fetching also sports two other convenient features, progress reporting and cancellation. An asynchronous fetch request can be cancelled at any time, for example, when the user decides the fetch request takes too long to complete. Progress reporting is a useful addition to show the user the current state of the fetch request.
Asynchronous fetching is a flexible API. Not only is it possible to cancel an asynchronous fetch request, it's also possible to make changes to the managed object context while the asynchronous fetch request is being executed. In other words, the user can continue to use your application while the application executes an asynchronous fetch request in the background.
3. How Does It Work?
Like batch updates, asynchronous fetch requests are handed to the managed object context as an NSPersistentStoreRequest object, an instance of the NSAsynchronousFetchRequest class to be precise.
An NSAsynchronousFetchRequest instance is initialized with an NSFetchRequest object and a completion block. The completion block is executed when the asynchronous fetch request has completed its fetch request.
Let's revisit the to-do application we created earlier in this series and replace the current implementation of the NSFetchedResultsController class with an asynchronous fetch request.
Step 1: Project Setup
Download or clone the project from GitHub and open it in Xcode 6. Before we can start working with the NSAsynchronousFetchRequest class, we need to make some changes. We won't be able to use the NSFetchedResultsController class for managing the table view's data since the NSFetchedResultsController class was designed to run on the main thread.
Step 2: Replacing the Fetched Results Controller
Start by updating the private class extension of the TSPViewController class as shown below. We remove the fetchedResultsController property and create a new property, items, of type NSArray for storing the to-do items. This also means that the TSPViewController class no longer needs to conform to the NSFetchedResultsControllerDelegate protocol.
@interface TSPViewController ()
@property (strong, nonatomic) NSArray *items;
@property (strong, nonatomic) NSIndexPath *selection;
@end
Before we refactor the viewDidLoad method, I first want to update the implementation of the UITableViewDataSource protocol. Take a look at the changes I've made in the following code blocks.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return <span class="skimlinks-unlinked">self.items</span> ? 1 : 0;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return <span class="skimlinks-unlinked">self.items</span> ? <span class="skimlinks-unlinked">self.items.count</span> :
- (void)configureCell:(TSPToDoCell *)cell atIndexPath:(NSIndexPath *)indexPath {
// Fetch Record
NSManagedObject *record = [<span class="skimlinks-unlinked">self.items</span> objectAtIndex:<span class="skimlinks-unlinked">indexPath.row</span>];
// Update Cell
[cell.nameLabel setText:[record valueForKey:@"name"]];
[cell.doneButton setSelected:[[record valueForKey:@"done"] boolValue]];
[cell setDidTapButtonBlock:^{
BOOL isDone = [[record valueForKey:@"done"] boolValue];
// Update Record
[record setValue:@(!isDone) forKey:@"done"];
}];
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObject *record = [<span class="skimlinks-unlinked">self.items</span> objectAtIndex:<span class="skimlinks-unlinked">indexPath.row</span>];
if (record) {
[self.managedObjectContext deleteObject:record];
}
}
}
We also need to change one line of code in the prepareForSegue:sender: method as shown below.
// Fetch Record
NSManagedObject *record = [<span class="skimlinks-unlinked">self.items</span> objectAtIndex:<span class="skimlinks-unlinked">self.selection.row</span>];
Last but not least, delete the implementation of the NSFetchedResultsControllerDelegate protocol since we no longer need it.
Step 3: Creating the Asynchronous Fetch Request
As you can see below, we create the asynchronous fetch request in the view controller's viewDidLoad method. Let's take a moment to see what's going on.
- (void)viewDidLoad {
[super viewDidLoad];
// Helpers
__weak TSPViewController *weakSelf = self;
// Initialize Fetch Request
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"TSPItem"];
// Add Sort Descriptors
[fetchRequest setSortDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"createdAt" ascending:YES]]];
// Initialize Asynchronous Fetch Request
NSAsynchronousFetchRequest *asynchronousFetchRequest = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult *result) {
dispatch_async(dispatch_get_main_queue(), ^{
// Process Asynchronous Fetch Result
[weakSelf processAsynchronousFetchResult:result];
});
}];
// Execute Asynchronous Fetch Request
[self.managedObjectContext performBlock:^{
// Execute Asynchronous Fetch Request
NSError *asynchronousFetchRequestError = nil;
NSAsynchronousFetchResult *asynchronousFetchResult = (NSAsynchronousFetchResult *)[weakSelf.managedObjectContext executeRequest:asynchronousFetchRequest error:&asynchronousFetchRequestError];
if (asynchronousFetchRequestError) {
NSLog(@"Unable to execute asynchronous fetch result.");
NSLog(@"%@, %@", asynchronousFetchRequestError, asynchronousFetchRequestError.localizedDescription);
}
}];
}
We start by creating and configuring an NSFetchRequest instance to initialize the asynchronous fetch request. It's this fetch request that the asynchronous fetch request will execute in the background.
// Initialize Fetch Request
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"TSPItem"];
// Add Sort Descriptors
[fetchRequest setSortDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"createdAt" ascending:YES]]];
To initialize an NSAsynchronousFetchRequest instance, we invoke initWithFetchRequest:completionBlock:, passing in fetchRequest and a completion block.
// Initialize Asynchronous Fetch Request
NSAsynchronousFetchRequest *asynchronousFetchRequest = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult *result) {
dispatch_async(dispatch_get_main_queue(), ^{
// Process Asynchronous Fetch Result
[weakSelf processAsynchronousFetchResult:result];
});
}];
The completion block is invoked when the asynchronous fetch request has completed executing its fetch request. The completion block takes one argument of type NSAsynchronousFetchResult, which contains the result of the query as well as a reference to the original asynchronous fetch request.
In the completion block, we invoke processAsynchronousFetchResult:, passing in the NSAsynchronousFetchResult object. We'll take a look at this helper method in a few moments.
Executing the asynchronous fetch request is almost identical to how we execute an NSBatchUpdateRequest. We call executeRequest:error: on the managed object context, passing in the asynchronous fetch request and a pointer to an NSError object.
[self.managedObjectContext performBlock:^{
// Execute Asynchronous Fetch Request
NSError *asynchronousFetchRequestError = nil;
NSAsynchronousFetchResult *asynchronousFetchResult = (NSAsynchronousFetchResult *)[weakSelf.managedObjectContext executeRequest:asynchronousFetchRequest error:&asynchronousFetchRequestError];
if (asynchronousFetchRequestError) {
NSLog(@"Unable to execute asynchronous fetch result.");
NSLog(@"%@, %@", asynchronousFetchRequestError, asynchronousFetchRequestError.localizedDescription);
}
}];
Note that we execute the asynchronous fetch request by calling performBlock: on the managed object context. While this isn't strictly necessary since the viewDidLoad method, in which we create and execute the asynchronous fetch request, is called on the main thread, it's a good habit and best practice to do so.
Even though the asynchronous fetch request is executed in the background, note that the executeRequest:error: method returns immediately, handing us an NSAsynchronousFetchResult object. Once the asynchronous fetch request completes, that same NSAsynchronousFetchResult object is populated with the result of the fetch request.
Finally, we check if the asynchronous fetch request was executed without issues by checking if the NSError object is equal to nil.
Step 4: Processing the Asynchronous Fetch Result
The processAsynchronousFetchResult: method is nothing more than a helper method in which we process the result of the asynchronous fetch request. We set the view controller's items property with the contents of the result's finalResult property and reload the table view.
- (void)processAsynchronousFetchResult:(NSAsynchronousFetchResult *)asynchronousFetchResult {
if (asynchronousFetchResult.finalResult) {
// Update Items
[self setItems:asynchronousFetchResult.finalResult];
// Reload Table View
[self.tableView reloadData];
}
}
Step 5: Build and Run
Build the project and run the application in the iOS Simulator. You may be surprised to see your application crash when it tries to execute the asynchronous fetch request. Fortunately, the output in the console tells us what went wrong.
1
2
3
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'NSConfinementConcurrencyType context <NSManagedObjectContext: 0x7fce3a731e60> cannot support asynchronous fetch request <NSAsynchronousFetchRequest: 0x7fce3a414300> with fetch request <NSFetchRequest: 0x7fce3a460860> (entity: TSPItem; predicate: ((null)); sortDescriptors: ((
"(createdAt, ascending, compare:)"
)); type: NSManagedObjectResultType; ).'
If you haven't read the article about Core Data and concurrency, you may be confused by what you're reading. Remember that Core Data declares three concurrency types, NSConfinementConcurrencyType, NSPrivateQueueConcurrencyType, and NSMainQueueConcurrencyType. Whenever you create a managed object context by invoking the class's init method, the resulting managed object context's concurrency type is equal to NSConfinementConcurrencyType. This is the default concurrency type.
The problem, however, is that asynchronous fetching is incompatible with the NSConfinementConcurrencyType type. Without going into too much detail, it's important to know that the asynchronous fetch request needs to merge the results of its fetch request with the managed object context that executed the asynchronous fetch request. It needs to know on which dispatch queue it can do this and that is why only NSPrivateQueueConcurrencyType and NSMainQueueConcurrencyType support asynchronous fetching. The solution is very simple though.
Step 6: Configuring the Managed Object Context
Open TSPAppDelegate.m and update the managedObjectContext method as shown below
- (NSManagedObjectContext *)managedObjectContext {
if (_managedObjectContext) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator) {
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
The only change we've made is replacing the init method with initWithConcurrencyType:, passing in NSMainQueueConcurrencyType as the argument. This means that the managed object context should only be accessed from the main thread. This works fine as long as we use the performBlock: or performBlockAndWait: methods to access the managed object context.
Run the project one more time to make sure that our change has indeed fixed the problem.
4. Showing Progress
The NSAsynchronousFetchRequest class adds support for monitoring the progress of the fetch request and it's even possible to cancel an asynchronous fetch request, for example, if the user decides that it's taking too long to complete.
The NSAsynchronousFetchRequest class leverages the NSProgress class for progress reporting as well as canceling an asynchronous fetch request. The NSProgress class, available since iOS 7 and OS X 10.9, is a clever way to monitor the progress of a task without the need to tightly couple the task to the user interface.
The NSProgress class also support cancelation, which is how an asynchronous fetch request can be canceled. Let's find out what we need to do to implement progress reporting for the asynchronous fetch request.
Step 1: Adding SVProgressHUD
We'll show the user the progress of the asynchronous fetch request using Sam Vermette's SVProgressHUD library. Download the library from GitHub and add the SVProgressHUD folder to your Xcode project.
Step 2: Setting Up NSProgress
In this article, we won't explore the NSProgress class in much detail, but feel free to read more about it in the documentation. We create an NSProgress instance in the block we hand to the performBlock: method in the view controller's viewDidLoad method.
// Create Progress
NSProgress *progress = [NSProgress progressWithTotalUnitCount:1];
// Become Current
[progress becomeCurrentWithPendingUnitCount:1];
You may be surprised that we set the total unit count to 1. The reason is simple. When Core Data executes the asynchronous fetch request, it doesn't know how many records it will find in the persistent store. This also means that we won't be able to show the relative progress to the user—a percentage. Instead, we will show the user the absolute progress—the number of records it has found.
You could remedy this issue by performing a fetch request to fetch the number of records before you execute the asynchronous fetch request. I prefer not to do this, though, because this also means that fetching the records from the persistent store takes longer to complete because of the extra fetch request at the start.
Step 3: Adding an Observer
When we execute the asynchronous fetch request, we are immediately handed an NSAsynchronousFetchResult object. This object has a progress property, which is of type NSProgress. It's this progress property that we need to observe if we want to receive progress updates.
// Execute Asynchronous Fetch Request
[self.managedObjectContext performBlock:^{
// Create Progress
NSProgress *progress = [NSProgress progressWithTotalUnitCount:1];
// Become Current
[progress becomeCurrentWithPendingUnitCount:1];
// Execute Asynchronous Fetch Request
NSError *asynchronousFetchRequestError = nil;
NSAsynchronousFetchResult *asynchronousFetchResult = (NSAsynchronousFetchResult *)[self.managedObjectContext executeRequest:asynchronousFetchRequest error:&asynchronousFetchRequestError];
if (asynchronousFetchRequestError) {
NSLog(@"Unable to execute asynchronous fetch result.");
NSLog(@"%@, %@", asynchronousFetchRequestError, asynchronousFetchRequestError.localizedDescription);
}
// Add Observer
[asynchronousFetchResult.progress addObserver:self forKeyPath:@"completedUnitCount" options:NSKeyValueObservingOptionNew context:ProgressContext];
// Resign Current
[progress resignCurrent];
}];
Note that we call resignCurrent on the progress object to balance the earlier becomeCurrentWithPendingUnitCount: call. Keep in mind that both of these methods need to be invoked on the same thread.
Step 4: Removing the Observer
In the completion block of the asynchronous fetch request, we remove the observer and dismiss the progress HUD.
// Initialize Asynchronous Fetch Request
NSAsynchronousFetchRequest *asynchronousFetchRequest = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult *result) {
dispatch_async(dispatch_get_main_queue(), ^{
// Dismiss Progress HUD
[SVProgressHUD dismiss];
// Process Asynchronous Fetch Result
[weakSelf processAsynchronousFetchResult:result];
// Remove Observer
[result.progress removeObserver:weakSelf forKeyPath:@"completedUnitCount" context:ProgressContext];
});
}];
Before we implement observeValueForKeyPath:ofObject:change:context:, we need to add an import statement for the SVProgressHUD library, declare the static variable ProgressContext that we pass in as the context when adding and removing the observer, and show the progress HUD before creating the asynchronous fetch request.
#import "SVProgressHUD/SVProgressHUD.h"
static void *ProgressContext = &ProgressContext;
- (void)viewDidLoad {
[super viewDidLoad];
// Helpers
__weak TSPViewController *weakSelf = self;
// Show Progress HUD
[SVProgressHUD showWithStatus:@"Fetching Data" maskType:SVProgressHUDMaskTypeGradient];
// Initialize Fetch Request
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"TSPItem"];
// Add Sort Descriptors
[fetchRequest setSortDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"createdAt" ascending:YES]]];
// Initialize Asynchronous Fetch Request
NSAsynchronousFetchRequest *asynchronousFetchRequest = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult *result) {
dispatch_async(dispatch_get_main_queue(), ^{
// Dismiss Progress HUD
[SVProgressHUD dismiss];
// Process Asynchronous Fetch Result
[weakSelf processAsynchronousFetchResult:result];
// Remove Observer
[result.progress removeObserver:weakSelf forKeyPath:@"completedUnitCount" context:ProgressContext];
});
}];
// Execute Asynchronous Fetch Request
[self.managedObjectContext performBlock:^{
// Create Progress
NSProgress *progress = [NSProgress progressWithTotalUnitCount:1];
// Become Current
[progress becomeCurrentWithPendingUnitCount:1];
// Execute Asynchronous Fetch Request
NSError *asynchronousFetchRequestError = nil;
NSAsynchronousFetchResult *asynchronousFetchResult = (NSAsynchronousFetchResult *)[weakSelf.managedObjectContext executeRequest:asynchronousFetchRequest error:&asynchronousFetchRequestError];
if (asynchronousFetchRequestError) {
NSLog(@"Unable to execute asynchronous fetch result.");
NSLog(@"%@, %@", asynchronousFetchRequestError, asynchronousFetchRequestError.localizedDescription);
}
// Add Observer
[asynchronousFetchResult.progress addObserver:self forKeyPath:@"completedUnitCount" options:NSKeyValueObservingOptionNew context:ProgressContext];
// Resign Current
[progress resignCurrent];
}];
}
Step 5: Progress Reporting
All that's left for us to do, is implement the observeValueForKeyPath:ofObject:change:context: method. We check if context is equal to ProgressContext, create a status object by extracting the number of completed records from the change dictionary, and update the progress HUD. Note that we update the user interface on the main thread.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == ProgressContext) {
dispatch_async(dispatch_get_main_queue(), ^{
// Create Status
NSString *status = [NSString stringWithFormat:@"Fetched %li Records", (long)[[change objectForKey:@"new"] integerValue]];
// Show Progress HUD
[SVProgressHUD setStatus:status];
});
}
}
5. Dummy Data
If we want to properly test our application, we need more data. While I don't recommend using the following approach in a production application, it's a quick and easy way to populate the database with data.
Open TSPAppDelegate.m and update the application:didFinishLaunchingWithOptions: method as shown below. The populateDatabase method is a simple helper method in which we add dummy data to the database.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Populate Database
[self populateDatabase];
...
return YES;
}
The implementation is straightforward. Because we only want to insert dummy data once, we check the user defaults database for the key @"didPopulateDatabase". If the key isn't set, we insert dummy data.
- (void)populateDatabase {
// Helpers
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
if ([ud objectForKey:@"didPopulateDatabase"]) return;
for (NSInteger i = 0; i < 1000000; i++) {
// Create Entity
NSEntityDescription *entity = [NSEntityDescription entityForName:@"TSPItem" inManagedObjectContext:self.managedObjectContext];
// Initialize Record
NSManagedObject *record = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];
// Populate Record
[record setValue:[NSString stringWithFormat:@"Item %li", (long)i] forKey:@"name"];
[record setValue:[NSDate date] forKey:@"createdAt"];
}
// Save Managed Object Context
[self saveManagedObjectContext];
// Update User Defaults
[ud setBool:YES forKey:@"didPopulateDatabase"];
}
The number of records is important. If you plan to run the application on the iOS Simulator, then it's fine to insert 100,000 or 1,000,000 records. This won't work as good on a physical device and will take too long to complete.
In the for loop, we create a managed object and populate it with data. Note that we don't save the changes of the managed object context during each iteration of the for loop.
Finally, we update the user defaults database to make sure the database isn't populated the next time the application is launched.
Great. Run the application in the iOS Simulator to see the result. You'll notice that it takes a few moments for the asynchronous fetch request to start fetching records and update the progress HUD.
6. Breaking Changes
By replacing the fetched results controller class with an asynchronous fetch request, we have broken a few pieces of the application. For example, tapping the checkmark of a to-do item doesn't seem to work any longer. While the database is being updated, the user interface doesn't reflect the change. The solution is fairly easy to fix and I'll leave it up to you to implement a solution. You should now have enough knowledge to understand the problem and find a suitable solution.
Conclusion
I'm sure you agree that asynchronous fetching is surprisingly easy to use. The heavy lifting is done by Core Data, which means that there's no need to manually merge the results of the asynchronous fetch request with the managed object context. Your only job is to update the user interface when the asynchronous fetch request hands you its results. Together with batch updates, it's a great addition to the Core Data framework.
This article also concludes this series on Core Data. You have learned a lot about the Core Data framework and you know all the essentials to use Core Data in a real application. Core Data is a powerful framework and, with the release of iOS 8, Apple has shown us that it gets better every year.
No comments:
Post a Comment