Mantle as a Model Layer for iOS and OS X Apps

Fri, Aug 23 2013

I recently gave a brief talk at the Cocoaheads meeting in Vienna. This is a summary.

The nice folks at GitHub released an open source model layer for iOS and OS X applications that they initially developed for their own GitHub OS X app. They called it Mantle.

Usually Core Data is the obvious choice as a model layer since it’s developed by Apple and therefore tested and well documented. However, it does not fit every use case. Especially if a lot of objects are written at once, Core Data gets painfully slow and also freezes up your user interface when the NSManagedObjectContext of the main thread synchronizes with another one.

To have better control over the performance one could opt to use SQLite directly, as it was actually required back in the dark days of iOS 2, when Core Data was not available for iOS yet. Since direct SQLite access involves a lot of boilerplate and ugly C function calls, a framework like FMDB could help.

However if you don’t have the need for complex SQL queries and only want to fetch and display data from a JSON API, Mantle is an ideal tool. Alternatively, it could also be used as an adapter between the API data and your Core Data NSManagedObject models.

Version 1.2 was the most recent release of Mantle at the time this article was written.

Core Data and FMDB are basically wrappers around SQLite and Mantle is a wrapper around NSCoder that eliminates a lot of the boilerplate that is usually required.

As a reminder, Apple defines NSCoder as

“(…) the interface used by concrete subclasses to transfer objects and other Objective-C data items between memory and some other format. This capability provides the basis for archiving and distribution.”

In order for an object to conform to the necessary interfaces and to be able to translate data from an API, without going into too much detail, the following methods would have to be implemented:

// Translate data from API to object
- (id)initWithDictionary:(NSDictionary *)dictionary;

// NSCoding
- (id)initWithCoder:(NSCoder *)coder;
- (void)encodeWithCoder:(NSCoder *)coder;

// NSCopying
- (id)copyWithZone:(NSZone *)zone;

// NSObject
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;

That leads to a lot of code and the additon or removal of a single property would result in changes in all of those methods. That’s definitely not clean code and leads to mistakes very easily.

Enter Mantle.

The provided MTLModel takes care of all of that. The only thing left to do is to implement a method like the following:

+ (NSDictionary *)JSONKeyPathsByPropertyKey
{
    return @{
        @"itemId": @"id",
        @"headline": @"title",
        @"description": @"text",
        @"author": @"user.name",
        @"updatedAt": @"updated_at"
      };
}

This information tells Mantle how to translate data from a given dictionary, which may have been retrieved from an API, to an object. The key of the returned dictionary specifies the property of the object, and the value specifies the corresponding key path in data dictionary.

NSError *error = nil;
DGLItem *item = [MTLJSONAdapter modelOfClass:[DGLItem class] fromJSONDictionary:apiDataDictionary error:&error];

Even better yet, Mantle can also translate the object back into a dictionary, that can then be used to post changes back to the API.

NSDictionary *apiDataDictionary = [MTLJSONAdapter JSONDictionaryFromModel:item];

Strings and numbers are handled automatically. Mantle needs to be told how to translate other types:

+ (NSValueTransformer *)updatedAtJSONTransformer {
    return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
        return [self.dateFormatter dateFromString:str];
    } reverseBlock:^(NSDate *date) {
        return [self.dateFormatter stringFromDate:date];
    }];
}

One thing Mantle does not offer out of the box is persistence. But since the models do confirm to the NSCoding interface, it is fairly straightforward to add it:

- (void)persist
{
    NSMutableData *data = [NSMutableData data];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    [self encodeWithCoder:archiver];
    [archiver finishEncoding];
    [data writeToFile:[[self class] storagePathForItemId:self.itemId] atomically:YES];
}

+ (id)loadItem:(NSString *)itemId
{
    id item = nil;
    NSString *path = [self storagePathForItemId:itemId];
    if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
          NSData *data = [NSData dataWithContentsOfFile:path];
          NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
         item = [[[self class] alloc] initWithCoder:unarchiver];
         [unarchiver finishDecoding];
    }
    return item;
}

+ (NSString *)storagePath
{
    NSString *cachesDirectory = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString *storagePath = [cachesDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"com.myapp.data/%@", NSStringFromClass([self class])]];

    if (![[NSFileManager defaultManager] fileExistsAtPath:storagePath]) {
        [[NSFileManager defaultManager] createDirectoryAtPath:storagePath withIntermediateDirectories:YES attributes:nil error:nil];
    }

    return storagePath;
}

+ (NSString *)storagePathForItemId:(NSString *)itemId
{
    return [NSString stringWithFormat:@"%@/%@.data", self.storagePath, itemId];
}

In the end, Mantle will you save you a lot of code and pain. Unfortunately, at the moment, its documentation is not very exhaustive but hopefully this article got you interested and will help you along the way.

comments powered by Disqus