Core Data is a powerful framework. It seems a lot like an ORM, but its advocates are quick to remind you that it’s “actually” an object persistence framework. I think that’s how they stomach not being able to run arbitrary SQL on their own database.
Needling aside, it’s the right choice for a lot of apps. When the user has a set of data that’s wholly on the device, reach for Core Data. In a lot of the cases I’ve worked on recently, however, I’ve found that Core Data functions as more of a cache for objects that canonically live on a server and present themselves through an API.
For cases like these, Core Data is tremendous overkill. Core Data was designed before the prevalance of web services and APIs. It was intended to represent an object graph in its entirety, rather than a small portion that has been downloaded from a service. Since you don’t have the whole dataset, you can’t even effectively query against it.
The costs to using Core Data are very high, since it’s so complex, and the benefits are pretty minimal. Primarily, these types of apps use it to persist the objects so that they’ll work in the subway or load quicker on the next launch. Fortunately, we can write a small amount of code to get this effect without having to conform to Core Data’s madness.
Foundation provides a protocol called NSCoding
, which is as simple and elegant as Core Data isn’t. By making your models conform to NSCoding
, you can easily use NSKeyedArchiver
and NSKeyedUnarchiver
to save your objects to disk. Many built-in objects, like collection types, already conform to NSCoding
, so you get those for free.
To actually perform the caching, let’s make simple object, called SKCache
. Caches can be finicky and cause bugs easily, so I’d like to make it very easy to enable and disable.
@implementation SKCache
static BOOL _enabled = YES;
+ (void)enable {
_enabled = YES;
}
+ (void)disable {
_enabled = NO;
}
Caches also need a name:
- (instancetype)initWithName:(NSString *)name {
self = [super init];
if (!_enabled || !name) self = nil;
if (!self) return nil;
_name = name;
return self;
}
From that name, they’ll figure out where in the file system to save themselves.
- (NSString *)hashedName {
return [self.name MD5String];
}
- (NSString *)cacheFilename {
return [self.hashedName stringByAppendingPathExtension:@"cache"];
}
- (NSString *)appCacheDirectory {
NSArray *searchPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
return searchPath.firstObject;
}
- (NSString *)cacheLocation {
return [[self.appCacheDirectory
stringByAppendingPathComponent:@"caches"]
stringByAppendingPathComponent:self.cacheFilename];
}
(Note all the short, simple methods. This object follows the pattern from Graduation.)
Once we have a place to save objects to and fetch objects from, we can easily do that:
- (void)saveObject:(id<NSCoding>)object {
[NSKeyedArchiver archiveRootObject:object toFile:self.cacheLocation];
}
- (id<NSCoding>)fetchObject {
return [NSKeyedUnarchiver unarchiveObjectWithFile:self.cacheLocation];
}
@end
This particular type of cache is designed to totally overwrite all of its contents. Blowing it away entirely every time the app gets fresh data ensures there are fewer synchronization bugs. When initializing with a name, you can easily increment the version number when the schema changes or if you accidentally add bad data to it. Since the true data lives on the server, the cache doesn’t need to be durable at all. Changing the name of the cache will just leave an extra file in the Caches
folder that iOS will clean up when it needs the space.
Once we have a quick and easy way of storing a model object (or array of model objects), we can get to using it. We can set up our cache inside a remote data source.
- (instancetype)init {
self = [super init];
if (!self) return nil;
_fetcher = //set up a fetcher
_cache = [[SKCache alloc] initWithName:@"com.khanlou.followers?forUser=1234"];
[self loadFromCache];
[self fetchData];
return self;
}
When we first load the data source up, we check the cache for any old content that we can show while we wait for fresh data from the server.
- (void)loadFromCache {
self.content = [self.cache fetchObject];
[self informDelegateOfUpdate];
}
Next, we fetch fresh data. When we get it, we can save it in the cache and tell the UI to update via a delegate message.
- (void)fetchData {
[self.fetcher fetchWithSuccessBlock:^(NSArray *results) {
self.content = results;
[self.cache saveObject:self.content];
[self informDelegateOfUpdate];
} failureBlock:^(NSError *error) {
//don't blow away self.content
}];
}
Lastly, we need to accessors to get at the data for our table view:
- (NSInteger)numberOfSections {
return 1;
}
- (NSInteger)numberOfObjectsInSection:(NSInteger)sectionIndex {
return self.content.count;
}
- (id)objectAtIndexPath:(NSIndexPath *)indexPath {
return self.content[indexPath.row];
}
There’s not much else to this technique. It seems too simple, almost trivial and useless, but I’ve found that it solves the problem very well in practice.
Your app might need a more robust cache; for example, you might need one that can be queried or one that needs to be durable. Core Data may still be right for you. If your app is one of the many that are backed by web services, however, your problems aren’t the same problems that Core Data was designed to solve, and you should examine simpler solutions.