Why You Should Use NSFetchedResultsController?

NSFetchedResultsController is a very useful class provided by the CoreData framework. It solves many performance issues you frequently run into while reading a large amount of data from database and displaying that data using a UITableview, UICollectionView or MKMapView. You should always use the fetched results controller unless you have a good reason not to. In this post, I would like to show you why using a fetched results controller is a good idea.

Consider an app that shows a list of news feed related to Apple products using a table view. We will call this app FeedLoader. When a row in feed table view is tapped, it shows more information about the feed in a detail view.

feed_loader_app.png

Initial Design #

We can approach the design of this app in multiple ways, but the one below will provide us a good context for discussing why not using a fetched results controller might not be a good idea.

without_fetchedresultscontroller_design.png

Although the figure above has a lot of boxes, the design is not that complicated. I will walk you through it. Not wanting to create a God class that consumes every responsibility in the app, I have created multiple classes each responsible for one thing. The app is primarily responsible for following tasks:

I won’t explain every aspect of the FeedLoader app in this post. I will highlight only those that are relevant to the discussion of NSFetchedResultsController. I have tried to write the code for FeedLoader in a clean way. Hopefully, you will be able to read it with relative ease.

Download News Feed #

Let’s assume that there exists a news feed service that provides API for fetching news published between certain dates. When the user enters the feed list view, the process of downloading the latest news feed is initiated. Once the feed is downloaded, we persist them in a local database. That way we don’t need to make unnecessary calls to the news feed service when we want to show the feed that was already downloaded in the past.

To keep things simple, we will read feed data from a JSON file stored locally instead of downloading it from a remote feed service. We can accommodate this change in our design by creating a protocol named FeedFetcher and a concrete class named FileFeedFetcher that conforms to this protocol.

@protocol FeedFetcher <NSObject>

- (void)fetchFeedWithCompletionHandler:
    (void(^)(id JSON, NSError *error))handler;

@end
@interface FileFeedFetcher : NSObject <FeedFetcher>
@end

@implementation FileFeedFetcher

- (void)fetchFeedWithCompletionHandler:
    (void (^)(id JSON, NSError *error))handler 
{
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"feeds"
                                                         ofType:@"json"];

    NSData *jsonData = [NSData dataWithContentsOfFile:filePath];
    NSError *error = nil;
    NSArray *jsonObjects = [NSJSONSerialization JSONObjectWithData:jsonData
                                                           options:0
                                                             error:&error];
    if (jsonObjects == nil) {
        NSLog(@"Unable to parse feeds JSON: %@", error.description);
        handler(nil, error);
    }
    else {
        handler(jsonObjects, nil);
    }
}

@end

In the future, when we actually download a list of feeds from a remote server, we can easily create a class named ServerFeedFetcher that conforms to the FeedFetcher protocol as well. We can then inject a server feed fetcher instead of a file feed fetcher without having to change anything in FeedManager.

feed_fetcher_interface.png

@implementation ObjectConfigurator

- (FeedManager *)feedManager {
    FeedManager *feedManager = [[FeedManager alloc] init];
    [feedManager setFeedFetcher:[[ServerFeedFetcher alloc] init]];
    // ...

    return feedManager;
}

@end

ObjectConfigurator provides a light-weight framework for injecting dependencies. You can checkout the create_basic_setup branch from FeedLoader repo to see its full implementation. Through out this blog post, I will ask you to checkout a specific branch so that you can see various stages of the app in code as we change its design iteratively.

Persist News Feed Locally #

Once the FeedManager has retrieved feed JSON (either from a remote service or a local JSON file), it tells FeedBuilder to build Feed objects from that JSON. FeedBuilder creates a new Feed object only if one with the same source URL already doesn’t exist in database. When the builder is done creating Feed objects, it will tell FeedDataManager to persist them in a local database. Finally, it returns the Feed objects back to the FeedManager.

FeedDataManager provides a layer of abstraction on top of Core Data. Instead of passing around the managed object context to any class that might need to query feed related data from the database and sprinkling complex fetch request code all over the place, we can simply ask FeedDataManager to perform specific data related task for us. For example, checking whether a Feed object with a specific source URL already exists or not.

Display News Feed #

FeedListViewController uses custom cells of type FeedCard to display the feed information in a UITableView. Rather than creating the cells itself, the list view controller delegates that task to an object that conforms to the FeedListDataSource protocol. (A default implementation of the FeedListDataSource protocol is provided in FeedListDefaultDataSource class). The data source accepts an array of Feed objects. When the table view needs to display a cell at a specific indexpath, it asks the data source to provide that cell. The data source then creates a FeedCard instance, populates it with feed data and gives it to the tableview.

Download and Cache Feed Image #

All information required to display a news feed is persisted in database except the image data. Feed objects store image URL but not the actual image data. A third party library called SDWebImage is used to asynchronously download images from a remote server and cache them locally on disk. SDWebImage adds a category to the UIImageView class. As a result, we can initiate the loading and caching of images by sending setImageWithURL:placeholderImage: message directly to the image view included in a feed card. Images are loaded only when needed, i.e., when a feed card is visible to the user.

UIImage *placeholder = [UIImage imageNamed:@"feedPlaceholderImage"];
[self.feedImageView sd_setImageWithURL:[NSURL URLWithString:feed.imageUrl]
                      placeholderImage:placeholder];

Caching Feed Data #

At this point, if you run the app second time, you will notice that the feed list is empty. The issue here is that FeedBuilder doesn’t create a new Feed object if one with the same source URL already exists in database.

@implementation FeedBuilder

- (NSArray *)feedsFromJSON:(NSArray *)JSON {
    NSMutableArray *feedsArray = [NSMutableArray array];

    for (NSDictionary *feedDict in JSON) {
        if ([self feedExistsInDatabase:feedDict]) {
            continue;
        }

        Feed *feed = [self.feedDataManager newFeed];
        [self fillInDetailsForFeed:feed
                    fromDictionary:feedDict];

        [feedsArray addObject:feed];
    }

    [self.feedDataManager saveData];
    return [feedsArray copy];
}

@end

First time we run the app, there are no Feed objects stored in the database at all. Therefore, the builder creates five new feed objects and returns them to the feed manager. On the second run, the feed builder doesn’t create any because all five feed items in feeds.json file have already been persisted in the database. As a result, the builder returns an empty array to the feed manager. To fix this issue, we need to return locally saved feeds if the builder doesn’t create new ones. We could easily accomplish that by adding following code to buildFeedsFromJSON: method in FeedManager:

if ([feeds count] == 0) {
    feeds = [self.feedDataManager allFeed];
}

However, that seems like a bit of a hack to me. Also, if you look inside feeds.json file, there are only five feed items in there. Therefore, loading them all into the feed list table view won’t cause any performance issues for now. In reality the number of news feed that needs to be displayed will be much higher than five. Although, Apple engineers have done a fantastic job of optimizing UITableView, we still need to take care of the issue of not loading too many table view rows up-front by ourselves. What we need here is a proper caching mechanism that will not only retrieve feeds from a local database if we are unable to fetch new ones from a remote server, but also is smart enough to not load too many rows unless the user needs them. This approach will make scrolling through the table view quite smooth. It will also prevent the overall memory footprint from increasing unnecessarily. Let’s start building a foundation for that caching mechanism. The first thing we need to do is create FeedCache class.

@class FeedBuilder;
@class FeedDataManager;

@interface FeedCache : NSObject

- (void)setFeedBuilder:(FeedBuilder *)feedBuilder;
- (void)setFeedDataManager:(FeedDataManager *)feedDataManager;

- (NSArray *)cachedFeed;
- (NSArray *)addFeedToCacheFromJSON:(NSArray *)feedJSON;

@end
@interface FeedCache ()

@property (nonatomic) FeedBuilder *feedBuilder;
@property (nonatomic) FeedDataManager *feedDataManager;

@end

@implementation FeedCache

- (NSArray *)cachedFeed {
    return [self.feedDataManager allFeedSortedByKey:@"publishedDate"
                                          ascending:NO];
}

- (NSArray *)addFeedToCacheFromJSON:(NSArray *)feedJSON {
    NSMutableArray *feeds = [NSMutableArray array];

    for (NSDictionary *feedDict in feedJSON) {
        if ([self feedExistsInDatabase:feedDict]) {
            continue;
        }

        Feed *feed = [self.feedDataManager newFeed];
        [self.feedBuilder fillInDetailsForFeed:feed
                                      fromJSON:feedDict];
        [feeds addObject:feed];
    }

    [self.feedDataManager saveData];
    [self sortFeedsByPublishedDate:feeds];

    return [feeds copy];
}

- (BOOL)feedExistsInDatabase:(NSDictionary *)feed {
    return [self.feedDataManager feedExistsWithSourceUrl:feed[@"url"]];
}

- (void)sortFeedsByPublishedDate:(NSMutableArray *)feeds {
    [feeds sortUsingComparator:^NSComparisonResult(Feed *feed1, Feed *feed2) {
        // The minus sign here has the effect of reversing
        // order from ascending to descending.
        return -[feed1.publishedDate compare:feed2.publishedDate];
    }];
}

@end

When asked for cached feeds, FeedCache returns all feeds in the database stored thus far. It also allows us to add new feeds into the cache. It essentially means taking the feed JSON, creating new instances of Feed objects from that JSON, saving them into the database and returning them back to the caller. Now FeedManager can delegate the task of building Feed objects from JSON to FeedCache instead of FeedBuilder.

- (void)buildFeedsFromJSON:(NSArray *)JSON {
    NSArray *newlyFetchedFeeds = 
        [self.feedCache addFeedToCacheFromJSON:JSON];

    if ([self.delegate respondsToSelector:
        @selector(feedManager:didReceiveFeeds:)])
    {
        [self.delegate feedManager:self 
                   didReceiveFeeds:newlyFetchedFeeds];
    }
}

Now when FeedManager is asked to fetch the latest feeds, it can simply return the ones in cache while the new ones are still being fetched.

- (NSArray *)fetchFeeds {
    [self.feedFetcher fetchFeedWithCompletionHandler:
        ^(id JSON, NSError *error) 
    {
        if (error) {
            NSLog(@"Unable to fetch feeds.");
        }
        else {
            [self buildFeedsFromJSON:JSON];
        }
    }];

    return [self.feedCache cachedFeed];
}

The original problem of empty list when we run the app second time can now be solved by displaying the cached feeds when user enters the feed list view.

- (void)viewDidLoad {
    // ...    
    [self fetchFeeds];
}

- (void)fetchFeeds {
    NSArray *feeds = [self.feedManager fetchFeeds];
    [self.dataSource setFeeds:feeds];
    [self.feedTableView reloadData];
}

Now that we can fetch feeds incrementally, we need to give the table view data source an ability to add new feeds to its collection.

@protocol FeedListTableDataSource 
    <UITableViewDataSource, UITableViewDelegate>

- (void)setFeeds:(NSArray *)feeds;
- (void)addFeeds:(NSArray *)feeds;

@end
@implementation FeedListTableDefaultDataSource
// ...

- (void)addFeeds:(NSArray *)feeds {
    [self setFeeds:[self.feeds arrayByAddingObjectsFromArray:feeds]];
}

@end

Finally, we need to insert new rows into the table view when we receive new feeds from a remote server.

@implementation FeedListViewController
//...

- (void)feedManager:(FeedManager *)manager
    didReceiveFeeds:(NSArray *)feeds
{
    NSLog(@"Feed manager did receive %ld feeds", (long)[feeds count]);
    [self.dataSource addFeeds:feeds];
    [self insertFeedsIntoTableView:feeds];
}

- (void)insertFeedsIntoTableView:(NSArray *)feeds {
    if ([feeds count] > 0) {
        NSMutableArray *newRows = [NSMutableArray array];
        for (int i = 0; i < [feeds count]; i++) {
            [newRows addObject:[NSIndexPath indexPathForRow:i
                                                  inSection:0]];
        }

        [self.feedTableView 
            insertRowsAtIndexPaths:newRows
                  withRowAnimation:UITableViewRowAnimationTop];
    }
}

@end

With the addition of caching, our initial design has evolved a bit as shown in figure below.

basic_caching.png

Following diagram shows feed data flow from end-to-end with caching in place.

feed_display_workflow_with_caching.png

You can checkout the add_caching branch from FeedLoader repo to see all the caching related code.

Now that we have a basic caching mechanism in place, we can make it more robust by providing a way to specify how many feeds we want to fetch rather than returning everything in the database. We can also remove feeds that are not visible to the user from the table view data source. Currently, it holds onto each feed added to its collection which is quite inefficient because the collection could grow infinitely as we keep fetching new feeds. As you can see the to-do list for a robust cache keeps growing and growing as we add new performance related requirements.

This is where the fetched results controller provided by Apple comes very handy. In addition to solving all caching related problems we have encountered so far, the fetched results controller also provides following:

NSFetchedResultsController to the Rescue #

Rather than going down a rabbit hole of implementing our own caching solution, let’s give NSFetchedResultsController a try. First thing we need to do is expose the managed object context used by our Core Data stack via the data manager. The fetched results controller interacts with Core Data directly via the managed object context. Ideally, I would have liked not to leak this implementation detail related to database to our business logic but if we want to use the fetched results controller we have no other choice.

You can checkout the replace_caching_implementation_with_fetched_results_controller branch from FeedLoader repo if you would like to follow along with the code listed in this section.

@interface FeedDataManager : NSObject
// ...
@property (nonatomic, readonly) NSManagedObjectContext 
    *managedObjectContext;

@end

Now instead of manually adding the new list of feed returned by FeedManager to the data source, we can simply give it a fetched results controller.

@implementation FeedListViewController

- (void)viewDidLoad {
    // ...
    [self.dataSource 
        setFetchedResultsController:self.fetchedResultsController];
}

@end

We also need to modify the FeedListTableDataSource protocol to take the fetched results controller instead of manually setting the feed array.

@protocol FeedListTableDataSource 
    <UITableViewDataSource, UITableViewDelegate>

- (void)setFetchedResultsController:
    (NSFetchedResultsController *)controller;

@end

When we create a fetched results controller, we need to give it a fetch request that contains details such as which entity to fetch and how many of them to fetch in a batch. We can also tell it to sort the result by certain attributes such as publishedDate. Here is the code that creates a fetched results controller:

- (NSFetchedResultsController *)fetchedResultsController {
    if (_fetchedResultsController == nil) {
        NSFetchRequest *fetchRequest =
            [NSFetchRequest fetchRequestWithEntityName:@"Feed"];

            NSSortDescriptor *sortByPublishedDate =
                [[NSSortDescriptor alloc] initWithKey:@"publishedDate"
                                            ascending:NO];

        fetchRequest.sortDescriptors = @[sortByPublishedDate];
        fetchRequest.fetchBatchSize = 10;

        NSManagedObjectContext *context =
            self.feedDataManager.managedObjectContext;

        _fetchedResultsController =
            [[NSFetchedResultsController alloc]
                initWithFetchRequest:fetchRequest
                managedObjectContext:context
                sectionNameKeyPath:nil
                cacheName:nil];

        _fetchedResultsController.delegate = self;
    }

    return _fetchedResultsController;
}

Finally, we need to implement the delegate methods the fetched results controller will call when the managed objects in Core Data are created, updated or deleted.

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.feedTableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.feedTableView;

    switch(type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:@[newIndexPath]
                             withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:@[indexPath]
                             withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:
            [tableView reloadRowsAtIndexPaths:@[indexPath]
                             withRowAnimation:UITableViewRowAnimationNone];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:@[indexPath]
                             withRowAnimation:UITableViewRowAnimationFade];

            [tableView insertRowsAtIndexPaths:@[newIndexPath]
                             withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller
  didChangeSection:(id )sectionInfo
           atIndex:(NSUInteger)sectionIndex
     forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.feedTableView
                insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.feedTableView
                deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                withRowAnimation:UITableViewRowAnimationFade];
            break;

        default:
            break;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.feedTableView endUpdates];
}

When the fetched results controller is about to start sending change notifications, it will call the controllerWillChangeContent delegate method. We need to prepare the feed table view for updates in that method by calling beginUpdates. If a Feed managed object is created, deleted or updated in database, we insert, delete or update a table view row respectively. When all current change notifications provided by the fetched results controller have been received, we need to tell the table view to process all pending updates by calling endUpdates in controllerDidChangeContent: delegate method.

Now we can remove all traces of FeedCache class and let the fetched results controller take over. Here is how the design looks after replacing our own caching implementation with the fetched results controller:

with_fetchedresultscontroller_design.png

The end-to-end feed data flow has also changed with introduction of the fetched results controller.

feed_display_workflow_with_nsfetchedresultscontroller.png

Conclusion #

You might be thinking that this blog post has been a giant waste of time and I agree with you. In the beginning I mentioned that you should always use a fetched results controller unless you have a good reason not to. I could have stopped there and moved on with our lives, but I went on and on to show you why it’s a good idea to do that. We started with a code base that didn’t have any caching mechanism in place. We implemented our own caching and then replaced it with a NSFetchedResultsController instance.

In one of the projects I worked on, I came very close to implementing functionalities already provided by NSFetchedResultsController. I thought I would share that experience with you so that you won’t waste time trying to reinvent the wheel like I did.

The full code for FeedLoader app is available on GitHub.

 
117
Kudos
 
117
Kudos

Now read this

Transitioning to Swift

iOS community has enthusiastically accepted Swift as the successor to Objective-C. iOS developers feel excited when they get to implement a new app or a feature in Swift. The open-source community has been voraciously contributing to the... Continue →