Core Data Tutorial for iOS: How To Use NSFetchedResultsController

Adam Burkepile Adam Burkepile

This post is also available in: Spanish, Korean

Screenshot of Batch Fetching Output

Screenshot of Batch Fetching Output

Update 4/17/12: Fully updated for iOS 5 (original post by Ray Wenderlich, update by Adam Burkepile).

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 Core Data tutorial 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 Core Data tutorial 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.

(You’re going to need access to the project we did in part 2. If you need a fresh copy, you can download it here.)

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 FBCDMasterViewController.h, removing out our old NSArray of failedBankInfos, and adding a new NSFetchedResultsController instead:

@interface FBCDMasterViewController : UITableViewController
 
@property (nonatomic,strong) NSManagedObjectContext* managedObjectContext;
@property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController;
 
@end

In the synthesize section in FBCDMasterViewController.m, remove old failedBankInfos synthesize statement and add:

@synthesize fetchedResultsController = _fetchedResultsController;

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 viewDidLoad):

- (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:managedObjectContext];
    [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:managedObjectContext sectionNameKeyPath:nil
            cacheName:@"Root"];
    self.fetchedResultsController = theFetchedResultsController;
    _fetchedResultsController.delegate = self;
 
    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  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];
 
    // 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];
           [tableView insertRowsAtIndexPaths:[NSArray
arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
           break;
    }
}
 
 
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )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!

You can download the project here (direct download).

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!

Adam Burkepile
Adam Burkepile

Adam Burkepile is currently a full-time Software Consultant and independent iOS developer. If he isn’t at the computer, he’s probably getting punched in the face at Krav Maga. Check out his latest app – Pocket No Agenda. You can reach him for work or just to chat at Twitter, Github, his website, or email.

User Comments

88 Comments

[ 1 , 2 , 3 , 4 , 5 , 6 ]
  • Hello
    Wonderful tutorial....

    //adds the array of items to the pop up button using some of the methods covered earlier
    // [myViewPopUp addItemsWithTitles:self.myArray];
    This works but I would like to add a tag


    So I did this...

    unsigned int i;
    for (i = 0; i < 4; i++) {
    myViewPopUp addItemsWithTitles:self.myViewSelected(i); //this is where the problem lies

    if ((myViewPopUp.title = @"One"))
    {
    tag = 0;
    }
    else
    if ((myViewPopUp.title = @"Two"))
    {
    tag = 1;
    }
    else
    if ((myViewPopUp.title = @"Three"))
    {
    tag = 2;
    }
    else
    if ((myViewPopUp.title = @"Four"))
    {
    tag = 3;
    }
    }


    What do I need to do?

    Thanks.

    lenpartico
    lenpartico
  • As of today at least, if we follow the steps to install AFNetworking mentioned here, it gives:

    Undefined symbols for architecture i386:
    "_SecCertificateCopyData", referenced from:
    -[AFURLConnectionOperation connection:willSendRequestForAuthenticationChallenge:] in AFURLConnectionOperation.o
    "_SecCertificateCreateWithData", referenced from:
    ___44+[AFURLConnectionOperation pinnedPublicKeys]_block_invoke in AFURLConnectionOperation.o
    "_SecPolicyCreateBasicX509", referenced from:
    ___44+[AFURLConnectionOperation pinnedPublicKeys]_block_invoke in AFURLConnectionOperation.o
    -[AFURLConnectionOperation connection:willSendRequestForAuthenticationChallenge:] in AFURLConnectionOperation.o
    "_SecTrustCopyPublicKey", referenced from:
    ___44+[AFURLConnectionOperation pinnedPublicKeys]_block_invoke in AFURLConnectionOperation.o
    -[AFURLConnectionOperation connection:willSendRequestForAuthenticationChallenge:] in AFURLConnectionOperation.o
    "_SecTrustCreateWithCertificates", referenced from:
    ___44+[AFURLConnectionOperation pinnedPublicKeys]_block_invoke in AFURLConnectionOperation.o
    -[AFURLConnectionOperation connection:willSendRequestForAuthenticationChallenge:] in AFURLConnectionOperation.o
    "_SecTrustEvaluate", referenced from:
    ___44+[AFURLConnectionOperation pinnedPublicKeys]_block_invoke in AFURLConnectionOperation.o
    -[AFURLConnectionOperation connection:willSendRequestForAuthenticationChallenge:] in AFURLConnectionOperation.o
    "_SecTrustGetCertificateAtIndex", referenced from:
    -[AFURLConnectionOperation connection:willSendRequestForAuthenticationChallenge:] in AFURLConnectionOperation.o
    "_SecTrustGetCertificateCount", referenced from:
    -[AFURLConnectionOperation connection:willSendRequestForAuthenticationChallenge:] in AFURLConnectionOperation.o
    ld: symbol(s) not found for architecture i386
    clang: error: linker command failed with exit code 1 (use -v to see invocation)

    Adding the Security framework fixes the problem.
    auspicious99
  • Nifty tutorial! When would I have to worry about the size of the fetched IDs in the first underlying query ever, e.g. if I wanted this automatic pagination on a table of tens of thousands of rows? I imagine at some point a simple NSFetchRequest that without any fetchLimit, even that first request for just IDs could be too much, no?
    qix
  • Hi Ray,

    Nice Tutorial there. I downloaded the sample project and just tried changing the textures and it does not work; It works only with the textures you have included; If I use bigger textures, it just draws in black; any clue on what might be wrong?

    Thanks
    Don
    donofdons
  • I have been following through the tutorial only changed some data and file names.
    Everything seems to run OK but on the line -

    _fetchedResultsController.delegate = self;

    I get a warning saying

    Assigning to 'id' from incompatible type 'UAEHigherEdMasterViewController *const __strong'

    "UAEHigherEdMasterViewController" is what I called "FBCDMasterViewController"
    Haddow
  • I get the same issue, following everything that was in the tutorial.
    chris_284
  • Hello,

    Two things should be done to successfully run the tutorial in Xcode 5 and iOS 7:

    1. As it is pointed out earlier, FBCDMasterViewController header file should indicate that it implements NSFetchedResultsControllerDelegate

    @interface FBCDMasterViewController : UITableViewController <NSFetchedResultsControllerDelegate>

    By this, we free ourselves from the warning.

    2. A problem arises when we run the app first time, then close it and rerun. We get the following:
    "FATAL ERROR: The persistent cache of section information does not match the current configuration."

    The cache seems to be NOT purged. Changing the cacheName:@"Root" to cacheName:nil solves it.

    For more information:
    http://stackoverflow.com/questions/2709 ... ng-a-cache

    Merter Sualp
    Solidath
  • Awesome tutorial! You should do a tutorial thats a follow up to this one and covers adding and changing information in the core data. :)
    ftblms65
  • Hi Adam,

    Can we configure NSFetchResultController to work with UIPickerViewController.
    Any help appreciated.
    icodebuster
  • Hi,
    Thank you for your helpful tutorial.
    I have problem when running the code from the 2nd time (even when I run the downloaded project). The app crash at line: if (![[self fetchedResultsController] performFetch:&error])
    Message:
    CoreData: FATAL ERROR: The persistent cache of section information does not match the current configuration. You have illegally mutated the NSFetchedResultsController's fetch request, its predicate, or its sort descriptor without either disabling caching or using +deleteCacheWithName:
    Can you help me with this
    Tuyet
  • Solidath in an earlier post wrote:
    2. A problem arises when we run the app first time, then close it and rerun. We get the following:
    "FATAL ERROR: The persistent cache of section information does not match the current configuration."

    The cache seems to be NOT purged. Changing the cacheName:@"Root" to cacheName:nil solves it.


    Hope that helps!
    Richard Caseyrcasey
  • icodebuster wrote:Hi Adam,

    Can we configure NSFetchResultController to work with UIPickerViewController.
    Any help appreciated.


    Most of the examples for using NSFetchedResultController involve a table view. However, the data source methods used by a picker view to determine content are very similar to those used for a table view.

    Table View --> Picker View
    numberOfSectionsInTableView --> numberOfComponentsInPickerView
    numberOfRowsInSection --> numberOfRowsInComponent
    cellForRowAtIndexPath --> titleForRow:forComponent:

    Hope that is enough to get you started. Have fun!
    Richard Caseyrcasey
  • Great Tutorial !!
    Thank you !!
    inderids
[ 1 , 2 , 3 , 4 , 5 , 6 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in September: iOS 8 App Extensions!

Sign Up - September

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Dani Arnaout

... 50 total!

Update Team

... 14 total!

Editorial Team

  • John Clem

... 23 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • Zihan Xu
  • Miguel Angel

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!