This is the third and final part of a series to help get you up to speed with the basics of Core Data quickly.
In the first part of the series,
we created a visual data model for our objects, ran a quick and dirty
test to make sure it works, and hooked it up to a table view so we could
see a list of our objects.
In the second part of the series,
we discussed how to import or preload existing data into Core Data so
that we have some good default data when our app starts up.
In this part of the series, we’re going to discuss how we can
optimize our app by using NSFetchedResultsController, to reduce memory
overhead and improve response time.
Why Use NSFetchedResultsController?
So far, we’re at exactly the same point we were using the SQLite3
method. However, we didn’t have to write nearly as much code (notice
the absence of a FailedBankDatabase class constructing raw SQL
statements), and adding other functionality such as insert/delete
operations would be much simpler.
However, there’s one notable thing that we could add pretty easily
with Core Data that could give us huge benefits to performance: use
NSFetchedResultsController.
Right now we’re loading all of the FailedBankInfo objects from the
database into memory at once. That might be fine for this app, but the
more data we have the slower this will be, and could have a detrimental
impact to the user.
Ideally we’d like to load only a subset of the rows, based on what
the user is currently looking at in the table view. Luckily, Apple has
made this easy for us by providing a great utility class called
NSFetchedResultsController.
So, start by opening up FailedBanksListViewController.h, removing out
our old NSArray of failedBankInfos, and adding a new
NSFetchedResultsController instead:
@interface FailedBanksListViewController : UITableViewController
<NSFetchedResultsControllerDelegate> {
NSFetchedResultsController *_fetchedResultsController;
NSManagedObjectContext *_context;
}
@property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController;
@property (nonatomic, retain) NSManagedObjectContext *context;
@end
|
In the synthesize section, remove old failedBankInfos synthesize statement and add:
@synthesize fetchedResultsController = _fetchedResultsController;
|
Then before we forget, set fetchedResultsController to nil inside the dealloc method:
self.fetchedResultsController = nil;
|
Another awesome thing about NSFetchedResultsController is you an set
it to nil upon viewDidUnload, which means that all of the data that is
in memory can be freed up in low memory conditions (and the view is
offscreen). All you have to do is set it to null in viewDidUnload (and
make sure it’s re-initialized in viewDidLaod):
- (void)viewDidUnload {
self.fetchedResultsController = nil;
}
|
Ok, now onto the fun part – creating our fetched results controller!
We are going to override the get method for our property so that it
checks to see if the fetched results controller exists first. If it
does exist, it will return it, otherwise it will create it.
Add the following function toward the top of the file:
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:@"FailedBankInfo" inManagedObjectContext:_context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:@"details.closeDate" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:_context sectionNameKeyPath:nil
cacheName:@"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
[fetchRequest release];
[theFetchedResultsController release];
return _fetchedResultsController;
}
|
This should look pretty familiar to the code that we used to have in
viewDidLoad, to create a fetch request to pull out the FailedBankInfo
objects. However, there’s a lot of new stuff here so let’s discuss…
First, any time we use an NSFetchedResultsController, we need to set a
sort descriptor on the fetch request. A sort descriptor is just a
fancy term for an object we set up to tell Core Data how we want our
results sorted.
The cool thing about sort descriptors is they are very powerful. Not
only can you sort on any property of the object you are returning, but
you can sort on properties of related objects – just like we see here!
We want to sort the objects based on the close date in the
FailedBankDetails, but still only receive the data in FailedBankInfo –
and Core Data can do this!
A very important part is the next statement – to set the batch size
on the fetch request to some small size. In fact, this is the very
reason we want to use the fetched results controller in this case. This
way, the fetched results controller will only retrieve a subset of
objects at a time from the underlying database, and automatically fetch
mroe as we scroll.
So once we finish tweaking the fetch request with the sort descriptor
and batch size, we just create a NSFetchedRequestController and pass in
the fetch request. Note it takes a few other parameters too:
- For the managed object context, we just pass in our context.
- The section name key path lets us sort the data into sections in our
table view. We could sort the banks by State if we wanted to, for
example, here.
- The cacheName the name of the file the fetched results controller
should use to cache any repeat work such as setting up sections and
ordering contents.
So now that we have a method to return a fetched results controller,
let’s modify our class to use it rather than our old array method.
First, update viewDidLoad as follows:
- (void)viewDidLoad {
[super viewDidLoad];
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
exit(-1); // Fail
}
self.title = @"Failed Banks";
}
|
All we do here is get a handle to our fetchedResultsController (which
implicitly creates it as well) and call performFetch to retrieve the
first batch of data.
Then, update numberOfRowsInSection:
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo =
[[_fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
|
And update cellForRowAtIndexPath like the following:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
FailedBankInfo *info = [_fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = info.name;
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@, %@",
info.city, info.state];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:CellIdentifier] autorelease];
}
// Set up the cell...
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
|
Note we split out part of the logic into a separate configureCell method – this is because we’ll need it later.
Ok one more thing – we need to implement the delegate methods for the
NSFetchedResultsController. The good news is these are mostly
boilerplate – I literally copied and pasted these from an Apple sample.
So just add these methods to the bottom of your file:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
// Reloading the section inserts a new row and ensures that titles are updated appropriately.
[tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
|
Compile and run your project, and it should look the same. However,
if you examine the debug output you will see something very amazing…
SELECT 0, t0.Z_PK FROM ZFAILEDBANKINFO t0 LEFT OUTER JOIN
ZFAILEDBANKDETAILS t1 ON t0.ZDETAILS = t1.Z_PK
ORDER BY t1.ZCLOSEDATE DESC
total fetch execution time: 0.0033s for 234 rows.
SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSTATE, t0.ZCITY,
t0.ZDETAILS FROM ZFAILEDBANKINFO t0 LEFT OUTER JOIN
ZFAILEDBANKDETAILS t1 ON t0.ZDETAILS = t1.Z_PK WHERE
t0.Z_PK IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ORDER BY t1.ZCLOSEDATE DESC LIMIT 20
total fetch execution time: 0.0022s for 20 rows.
SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSTATE, t0.ZCITY,
t0.ZDETAILS FROM ZFAILEDBANKINFO t0 LEFT OUTER JOIN
ZFAILEDBANKDETAILS t1 ON t0.ZDETAILS = t1.Z_PK WHERE
t0.Z_PK IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ORDER BY t1.ZCLOSEDATE DESC LIMIT 20
total fetch execution time: 0.0017s for 20 rows.
You can see here that behind the scenes, the
NSFetchedResultsController is getting a list of IDs from FailedBankInfo,
in the proper sort order. Then, as the user pages through the table,
it loads one batch at a time – rather than loading all of the objects
into memory at once!
This would have been a lot more code to do with raw SQLite – and is
just one of the many reasons why using Core Data can save time and
increase performance.
Show Me The Code!
Here is a sample project with all of the code we have developed in the above tutorial.
Where to Go From Here?
You should have a good understanding of the basics of Core Data at
this point. A good exercise would be to continue this example to add in
the detail view that we created in the SQLite tutorial, or to add in
support for adding/editing/deleting items.
I’d recommend also taking a look at some of the Apple samples out
there – they have 5 different Core Data examples currently available and
they all show different and interesting aspects of things you can do.
Also, if you have any advice about Core Data or gotchas that you’ve encountered with Core Data in your projects, please share!