Could Core Data be a little more concise?

At the latest Beginning iOS Dev Workshop this September, I was showing my students some Core Data by walking through the template application that Xcode gives you if you create a new master-detail Core-Data-backed iOS application. If you haven’t done so yourself, you should check it out, because it presents a pretty nice way of constructing a pair of view controllers that give the user access to a list of Core Data objects. (EDIT 2013-02-20: fixed small error in fetchRequestForEntityNamed:sortedByKeys:fetchBatchSize: method)

The downside of that project template is that if you don’t already know how it works, it’s not that straightforward to figure out what’s going on, especially in the “master” controller where a lot of things happen through indirection via delegate methods, etc. Going through all this, I was struck by one method in particular: the fetchedResultsController method in the MasterViewController class:

- (NSFetchedResultsController *)fetchedResultsController
{
    if (_fetchedResultsController != nil) {
        return _fetchedResultsController;
    }
    
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];
    
    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize:20];
    
    // Edit the sort key as appropriate.
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timeStamp" ascending:NO];
    NSArray *sortDescriptors = @[sortDescriptor];
    
    [fetchRequest setSortDescriptors:sortDescriptors];
    
    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:@"Master"];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;
    
    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error]) {
         // Replace this implementation with code to handle the error appropriately.
         // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    
    return _fetchedResultsController;
}    

Objective-C is known for its verbosity, but that’s pretty extreme, considering what this method is designed to do: return a controller object that is set up to do the equivalent of a very simple SQl query:

SELECT * from Event
ORDER BY timeStamep DESC
LIMIT 20;

It’s not exactly the same, of course. Core Data also allows us to cluster the results together into sections, something that SQL doesn’t support directly (imagine something like GROUP BY, but instead of lumping results together, it that would still return the same basic list of results, but grouped into multiple arrays based on the content of one column).

The start of the method contains boilerplate, basically to check whether the method has already been called or not. The end of the method actually kicks off the query which runs in the background and can’t really be cleaned up too much. Everything in between is what seems to be just too big.

Essentially, lines 7-10 of this method are the “SELECT”, line 13 is the “LIMIT”, lines 15-19 are the “ORDER BY”, and lines 21-25 take care of the sectioning. So we’re looking at roughly 20 lines of Objective-C code (some of them very long lines as well) corresponding to 3 short lines of SQL (which would perhaps be 4 lines if SQL supported the sectioning that Core Data does). Some of my students found this pretty troubling, and I can totally see why. There’s a huge trend in programming toward higher-level APIs that let you do more with fewer lines of code. Objective-C will never be as succinct as SQL, but surely we can do better than this.

So I started looking at the various pieces used to build up and execute this query, to see what prerequites they have and how they are tied together. The first thing the method creates is an NSFetchRequest. This is a “bare object” that has no context initially, but will eventually contain all the parameters needed for the query we want to run. We start off by grabbing an entity (by name) from the NSManagedObjectContext to assign to the fetch request, then we set its batch size. After that, we create a new NSSortDescriptor, which contains the name of an attribute, and a BOOL indicating the sort order. This object is put into an array, which is then passed to the fetch request (which can take an array so that you can provide multiple sorting attributes). Finally we create an NSFetchResultsController, passing in the fetch request, the managed object context, a sectioning key-path (unused here) and the name of a cache to hold the results.

Throughout all of that, I found a number of spots where a string is used to grab or create another object simply because it was needed as a parameter to another method. These intermediate objects (an NSEntityDescription and an NSSortDescriptor) are not useful in this method otherwise, they’re just being passed elsewhere. I also found that a couple of the things being grabbed or created need the managed object context. With these things in mind, I created an alternate API, in the form of a new method in NSMangedObjectContext, that would build all these things for us, using simple strings where appropriate instead of intermediaries, and give us back an NSFetchedObjectController. With this API in place, the fetchedResultsController method becomes this:

- (NSFetchedResultsController *)fetchedResultsController
{
    if (_fetchedResultsController != nil) {
        return _fetchedResultsController;
    }
    
    self.fetchedResultsController = [self.managedObjectContext
                                     fetchedResultsControllerForEntityNamed:@"Event"
                                     sortedByKeys:@[@"-timeStamp"]
                                     fetchBatchSize:20
                                     sectionNameKeyPath:nil
                                     cacheName:@"Master"];
    self.fetchedResultsController.delegate = self;
    
    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error]) {
        // Replace this implementation with code to handle the error appropriately.
        // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    
    return _fetchedResultsController;
}

As you can see, the beginning and ending are identical, but all that stuff in the middle shrunk from 19 lines down to 7. To top it off, it’s now a lot easier to read, since you can see all the pieces together and digest it at once, just like you can with SQL.

I skipped the NSEntityDescription and just use an entity name instead. I simplified the sorting specification by eliminating the intermediate NSSortDescriptors; Instead, you just pass an array of strings. If the first character is a “-“, it’s the equivalent of using “ascending:NO” when creating an NSSortDescriptor, so sorting for that attribute will be reversed. And even though this example app doesn’t use sections, I included the parameter in this new API (it’s passed straight through to the new fetched results controller) for maximum flexibility.

I’ve implemented this in a category on NSManagedObjectContext, shown here:

//
//  NSManagedObjectContext+Simpler.h
//  SimplerCoreData
//
//  Created by Jack Nutting on 10/31/12.
//  Copyright (c) 2012 Rebisoft. All rights reserved.
//

#import <CoreData/CoreData.h>

@interface NSManagedObjectContext (Simpler)

- (NSFetchRequest *)fetchRequestForEntityNamed:(NSString *)entityName
                                  sortedByKeys:(NSArray *)keys
                                fetchBatchSize:(NSUInteger)fetchBatchSize;

- (NSFetchedResultsController *)fetchedResultsControllerForEntityNamed:entityName
                                                          sortedByKeys:(NSArray *)keys
                                                        fetchBatchSize:(NSUInteger) fetchBatchSize
                                                    sectionNameKeyPath:(NSString *)sectionNameKeyPath
                                                             cacheName:(NSString *)cacheName;
@end
//
//  NSManagedObjectContext+Simpler.m
//  SimplerCoreData
//
//  Created by Jack Nutting on 10/31/12.
//  Copyright (c) 2012 Rebisoft. All rights reserved.
//

#import "NSManagedObjectContext+Simpler.h"

@implementation NSManagedObjectContext (Simpler)

- (NSFetchRequest *)fetchRequestForEntityNamed:(NSString *)entityName
                                  sortedByKeys:(NSArray *)keys
                                fetchBatchSize:(NSUInteger)fetchBatchSize {
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:self];
    [fetchRequest setEntity:entity];

    [fetchRequest setFetchBatchSize:fetchBatchSize];
    
    NSMutableArray *sortDescriptors = [NSMutableArray arrayWithCapacity:[keys count]];
    NSString *key;
    for (key in keys) {
        BOOL ascending = YES;
        if ([key characterAtIndex:0] == '-') {
            ascending = NO;
            key = [key substringFromIndex:1];
        }
        NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:key ascending:ascending];
        [sortDescriptors addObject:sortDescriptor];
    }
    
    [fetchRequest setSortDescriptors:sortDescriptors];

    return fetchRequest;
}

- (NSFetchedResultsController *)fetchedResultsControllerForEntityNamed:entityName
                                                          sortedByKeys:(NSArray *)keys
                                                        fetchBatchSize:(NSUInteger)fetchBatchSize
                                                    sectionNameKeyPath:(NSString *)sectionNameKeyPath
                                                             cacheName:(NSString *)cacheName {
    NSFetchRequest *fetchRequest = [self fetchRequestForEntityNamed:entityName sortedByKeys:keys fetchBatchSize:fetchBatchSize];
    
    NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self sectionNameKeyPath:sectionNameKeyPath cacheName:cacheName];
    return fetchedResultsController;
}

@end

I broke it up into two methods: The one I showed in the improved fetchResultsController method, which in turn calls another method that creates a fetch request but doesn’t attach it to a controller. This method could be useful in its own right, since it lets you create a fetch request using the same shortcuts for entity and sorting keys that I established earlier.

I thought about putting this on github, but for such a short example I don’t think there’s really much point. Hopefully this demonstrates how to extend existing classes to give you a nice higher-level API.

Comments