17 April 2010

Core Data Tutorial: How To Use NSFetchedResultsController

Screenshot of Batch Fetching Output

Screenshot of Batch Fetching Output

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!

Category: iPhone

Tags: , , ,

18 Comments

  1. Alexei Vinidiktov (2 comments) says:

    Hi Ray!

    Thanks for the tutorial! It’s really helpful.

    While working through it I noticed a small typo in this snippet:

    - (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section {
    id sectionInfo =
    [[fetchedResultsController sections] objectAtIndex:section];
    return [sectionInfo numberOfObjects];
    }

    I had to change the line

    [[fetchedResultsController sections] objectAtIndex:section];

    to:
    [[_fetchedResultsController sections] objectAtIndex:section];

    And also earlier in the text you said:

    In the synthesize section, remove old failedBankInfos synthesize statement and add:
    @synthesize context = _context;

    whereas you probably meant to say:

    @synthesize fetchedResultsController = _fetchedResultsController;

  2. Alexei Vinidiktov (2 comments) says:

    And also in this method:

    - (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];
    }

    I had to change the line

    FailedBankInfo *info = [fetchedResultsController objectAtIndexPath:indexPath];

    to either:

    FailedBankInfo *info = [_fetchedResultsController objectAtIndexPath:indexPath];

    or:

    FailedBankInfo *info = [self.fetchedResultsController objectAtIndexPath:indexPath];

  3. Ray Wenderlich (492 comments) says:

    @Alexei: D’oh! Thanks for pointing those out those typos, I’ve fixed the blog post above!

  4. Mike Young (2 comments) says:

    Hi, Ray,

    I’m entirely new to app dev for the iPad/iPhone and find your tutorials most helpful in learning the process. I learn best by following the designs of others and then adapting them to my ideas.

    In particular, the app I have in mind is very ambitious and must involve core data and must involve split views on the iPad for effective navigation and UI.

    Your tutorials start out with split view on the iPad and later move to considerations of core data, so this looks to be exactly what I need to get started. However, the core data tutorial (no. 3) is an iPhone app rather than an extension of core data in the split view iPad environment.

    My other model for learning is Apple’s CoreDataBooks sample code, which if recast into the iPad split view design, would be even more helpful in that it uses the grouped list and navigation bar buttons for hierarchy and editing.

    I wonder if your tutorials could move to this level of design including headers and footers extracted from the core data to show a more structured view of the data for navigation. For example, grouping the banks by state and then building in the edit and add buttons in the navigation bar.

    Once again, thanks for your willingness to share.

  5. Ray Wenderlich (492 comments) says:

    @Mike: Sounds like a good idea! I wrote the Core Data tutorials first and then the iPad tutorials after, which is why the Core Data tutorials are for the iPhone.

    I actually considered making the iPad examples involve Core Data, but decided against it to help keep things simple. My current hope is that if people go through the Core Data tutorials and iPad tutorials separately, they should have enough background knowledge in order to combine the two in one of their own apps.

    However Core Data is a deep subject, so perhaps I will revisit it some day – and if so your post is a good idea for how to do so! :]

  6. Mike Young (2 comments) says:

    Thanks, Ray,

    As you know, the iPad gives us so much more real estate with which to play that enterprise apps that require data entry as well as retrieval and display of complex data are vastly more likely on this platform.

    My app is or I hope will be a port of a very extensive real estate app I wrote back in the late ’70s on a Wang mini. Now, with the iPad and its ease of use and the possibility of adding graphs and maps and photos, the time is ripe to revive my enterprise app with the latest technology. I hope to pitch it toward students and professionals alike built on the same database structure.

    I’m having a hard time getting the basic split view, editing, adding, grouping UI, and core data hooks to work properly (or at all in some cases), but hope that once I can crack the code, the real app development can grow rapidly. I’ll keep pounding away.

    I’ve looked at all the books I can find and at all the resources on the web, but none are directly on point, which surprises me a bit because the structure that I have outlined seems so obvious for building many, many enterprise apps.

    Thanks, again for your effort.

  7. James (3 comments) says:

    Hi, Ray.

    Thanks for the tutorials!

    I’m hoping to apply what I’ve learned to a situation where I’m using multiple database tables and I wonder if you have any thoughts as to how to go about adapting the FetchedResultsController to handle that.

    For example, say I have a database table for employees and another one for workgroups. It’s a many-to-many relationship and I have viewTables for both with buttons that allow me to switch between them.

    I’ve populated the database and I can see one of the tables upon startup with the proper data – they both work ok on startup. But when I press the button to switch views, I can’t seem to get fetchedResultsController to fetch the new data.

    I’ve tried setting fetchedResultsController to nil to force a re-define, calling reloadData on the viewTable, setting the cache argument to nil, and fiddling with controllerWillChangeContent but to no avail.

    I wonder if you have any advice as to how to best proceed?

    Thanks,
    James

  8. Ray Wenderlich (492 comments) says:

    @James: I haven’t played with that situation myself, but on first thought, why not just a) have two different NSFetchedResultControllers, b) have a state variable which one you want to use and have the table view use the appropriate one based on the variable, c) switch the state variable and reload the table view upon a switch?

  9. James (3 comments) says:

    Hi, Ray.

    Thanks – your first thought worked out!

    It wasn’t obvious to me that I couldn’t just re-use the NSFetchedResultsController object over two different data sets. I thought if I’d just reset the entity, set the cacheName to nil, then I’d be able to have it re-fetch things…maybe holdover thinking from SQL stuff.

    Thanks again!
    James

  10. Ray Wenderlich (492 comments) says:

    @James: Awesome, glad it worked! :]

  11. Mark Spritzler (1 comments) says:

    Hi Ray, great job on your tutorials, very will written and detailed. Tutorials like that take a lot of time, and we all appreciate you taking that time to help us.

    I think a previous comment is similar to what I wanted to ask.

    I have a self referential table. a Folder can have a Collection of subfolders, etc. So if a user selects a folder in the tableView it needs to then go deeper and display its subfolders.

    Can this be done with a single FetchResultsController? If so, how? I was thinking the FetchResultsController could have a property to hold the parent Folders id, nil if it is the top parent, then include it in the predicate if it is not nil. Hmm, but then how to remove … Wait, in my didSelect… method I create a new instance of my TableViewController and push it onto the navigation controller stack. So I guess I just have a instance of FetchResultsController for each TableViewController and set the folder id in it like I stated above.

    Yes, I think I found my solution while typing this, but I am still going to post it, just in case someone else out there has the same question.

  12. Ray Wenderlich (492 comments) says:

    @Mark: Thanks! Yeah these tutorials take a crazy amount of work, so I’m glad they’ve come in handy to some people :]

    The solution you mentioned sounds like it should work – thanks for sharing for others!

  13. Ali (3 comments) says:

    hi ray ,
    thanks for your all part of tutorial on core data thats solve all of my problem in it and i learn to work with this completely,you are a great developer .

  14. Marc (1 comments) says:

    Does setting the NSFetchedResultsController to nil in viewDidUnload create a memory leak? If not then why not? You retained it when you assigned it to self.fetchedResultsController so setting it to nil without releasing it should create leak. Sorry if this is a bit basic, but I am still learning.

  15. Ray Wenderlich (492 comments) says:

    @Ali: Thanks so much for the kind words!

    @Marc: When you call self.fetchedResultsController = nil, it is actually the same as calling [self setFetchedResultsController:nil].

    This method setFetchedResultsController is automatically generated by the compiler when you call this method:

    @synthesize fetchedResultsController = _fetchedResultsController.

    Because the property was declared as retain, the generated property method is smart enough to release the old object before setting it to a new value. So when you call self.fetchedResultsController = nil, it’s actually releasing any old value, then setting the instance variable to nil!

  16. glj (1 comments) says:

    thanks for the great tutorial. if you wanted to split the results into different sections how would you do that?

  17. Ray Wenderlich (492 comments) says:

    @glj: For details on how to do that, I recommend Chapter 3 of More iPhone 3 Development by Dave Mark and Jeff LaMarche.

Leave a Comment

I'd love to hear your thoughts!

Tip: Want a cool picture next to your comment? Create a Gravatar!