# Mantle

Mantle makes it easy to write a simple model layer for your Cocoa or Cocoa Touch
application.

## The Typical Model Object

What's wrong with the way model objects are usually written in Objective-C?

Let's use the [GitHub API](http://developer.github.com) for demonstration. How
would one typically represent a [GitHub
issue](http://developer.github.com/v3/issues/#get-a-single-issue) in
Objective-C?

```objc
typedef enum : NSUInteger {
    GHIssueStateOpen,
    GHIssueStateClosed
} GHIssueState;

@interface GHIssue : NSObject <NSCoding, NSCopying>

@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, copy, readonly) NSDate *updatedAt;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *retrievedAt;

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;

- (id)initWithDictionary:(NSDictionary *)dictionary;

@end
```

```objc
@implementation GHIssue

+ (NSDateFormatter *)dateFormatter {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
    dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
    return dateFormatter;
}

- (id)initWithDictionary:(NSDictionary *)dictionary {
    self = [self init];
    if (self == nil) return nil;

    _URL = [NSURL URLWithString:dictionary[@"url"]];
    _HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]];
    _number = dictionary[@"number"];

    if ([dictionary[@"state"] isEqualToString:@"open"]) {
        _state = GHIssueStateOpen;
    } else if ([dictionary[@"state"] isEqualToString:@"closed"]) {
        _state = GHIssueStateClosed;
    }

    _title = [dictionary[@"title"] copy];
    _retrievedAt = [NSDate date];
    _body = [dictionary[@"body"] copy];
    _reporterLogin = [dictionary[@"user"][@"login"] copy];
    _assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]];

    _updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];

    return self;
}

- (id)initWithCoder:(NSCoder *)coder {
    self = [self init];
    if (self == nil) return nil;

    _URL = [coder decodeObjectForKey:@"URL"];
    _HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
    _number = [coder decodeObjectForKey:@"number"];
    _state = [coder decodeUnsignedIntegerForKey:@"state"];
    _title = [coder decodeObjectForKey:@"title"];
    _retrievedAt = [NSDate date];
    _body = [coder decodeObjectForKey:@"body"];
    _reporterLogin = [coder decodeObjectForKey:@"reporterLogin"];
    _assignee = [coder decodeObjectForKey:@"assignee"];
    _updatedAt = [coder decodeObjectForKey:@"updatedAt"];

    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
    if (self.URL != nil) [coder encodeObject:self.URL forKey:@"URL"];
    if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURL forKey:@"HTMLURL"];
    if (self.number != nil) [coder encodeObject:self.number forKey:@"number"];
    if (self.title != nil) [coder encodeObject:self.title forKey:@"title"];
    if (self.body != nil) [coder encodeObject:self.body forKey:@"body"];
    if (self.reporterLogin != nil) [coder encodeObject:self.reporterLogin forKey:@"reporterLogin"];
    if (self.assignee != nil) [coder encodeObject:self.assignee forKey:@"assignee"];
    if (self.updatedAt != nil) [coder encodeObject:self.updatedAt forKey:@"updatedAt"];

    [coder encodeUnsignedInteger:self.state forKey:@"state"];
}

- (id)copyWithZone:(NSZone *)zone {
    GHIssue *issue = [[self.class allocWithZone:zone] init];
    issue->_URL = self.URL;
    issue->_HTMLURL = self.HTMLURL;
    issue->_number = self.number;
    issue->_state = self.state;
    issue->_reporterLogin = self.reporterLogin;
    issue->_assignee = self.assignee;
    issue->_updatedAt = self.updatedAt;

    issue.title = self.title;
    issue->_retrievedAt = [NSDate date];
    issue.body = self.body;

    return issue;
}

- (NSUInteger)hash {
    return self.number.hash;
}

- (BOOL)isEqual:(GHIssue *)issue {
    if (![issue isKindOfClass:GHIssue.class]) return NO;

    return [self.number isEqual:issue.number] && [self.title isEqual:issue.title] && [self.body isEqual:issue.body];
}

@end
```

Whew, that's a lot of boilerplate for something so simple! And, even then, there
are some problems that this example doesn't address:

 * There's no way to update a `GHIssue` with new data from the server.
 * There's no way to turn a `GHIssue` _back_ into JSON.
 * `GHIssueState` shouldn't be encoded as-is. If the enum changes in the future,
   existing archives might break.
 * If the interface of `GHIssue` changes down the road, existing archives might
   break.

## Why Not Use Core Data?

Core Data solves certain problems very well. If you need to execute complex
queries across your data, handle a huge object graph with lots of relationships,
or support undo and redo, Core Data is an excellent fit.

It does, however, come with a couple of pain points:

 * **There's still a lot of boilerplate.** Managed objects reduce some of the
   boilerplate seen above, but Core Data has plenty of its own. Correctly
   setting up a Core Data stack (with a persistent store and persistent store
   coordinator) and executing fetches can take many lines of code.
 * **It's hard to get right.** Even experienced developers can make mistakes
   when using Core Data, and the framework is not forgiving.

If you're just trying to access some JSON objects, Core Data can be a lot of
work for little gain.

Nonetheless, if you're using or want to use Core Data in your app already,
Mantle can still be a convenient translation layer between the API and your
managed model objects.

## MTLModel

Enter
**[MTLModel](https://github.com/github/Mantle/blob/master/Mantle/MTLModel.h)**.
This is what `GHIssue` looks like inheriting from `MTLModel`:

```objc
typedef enum : NSUInteger {
    GHIssueStateOpen,
    GHIssueStateClosed
} GHIssueState;

@interface GHIssue : MTLModel <MTLJSONSerializing>

@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *updatedAt;

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;

@property (nonatomic, copy, readonly) NSDate *retrievedAt;

@end
```

```objc
@implementation GHIssue

+ (NSDateFormatter *)dateFormatter {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
    dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
    return dateFormatter;
}

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{
        @"URL": @"url",
        @"HTMLURL": @"html_url",
        @"reporterLogin": @"user.login",
        @"assignee": @"assignee",
        @"updatedAt": @"updated_at"
    };
}

+ (NSValueTransformer *)URLJSONTransformer {
    return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}

+ (NSValueTransformer *)HTMLURLJSONTransformer {
    return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}

+ (NSValueTransformer *)stateJSONTransformer {
    return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
        @"open": @(GHIssueStateOpen),
        @"closed": @(GHIssueStateClosed)
    }];
}

+ (NSValueTransformer *)assigneeJSONTransformer {
    return [NSValueTransformer mtl_JSONDictionaryTransformerWithModelClass:GHUser.class];
}

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

- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
    self = [super initWithDictionary:dictionaryValue error:error];
    if (self == nil) return nil;

    // Store a value that needs to be determined locally upon initialization.
    _retrievedAt = [NSDate date];

    return self;
}

@end
```

Notably absent from this version are implementations of `<NSCoding>`,
`<NSCopying>`, `-isEqual:`, and `-hash`. By inspecting the `@property`
declarations you have in your subclass, `MTLModel` can provide default
implementations for all these methods.

The problems with the original example all happen to be fixed as well:

> There's no way to update a `GHIssue` with new data from the server.

`MTLModel` has an extensible `-mergeValuesForKeysFromModel:` method, which makes
it easy to specify how new model data should be integrated.

> There's no way to turn a `GHIssue` _back_ into JSON.

This is where reversible transformers really come in handy. `+[MTLJSONAdapter
JSONDictionaryFromModel:]` can transform any model object conforming to
`<MTLJSONSerializing>` back into a JSON dictionary. `+[MTLJSONAdapter
JSONArrayForModels:]` is the same but turns an array of model objects into an JSON array of dictionaries.

> If the interface of `GHIssue` changes down the road, existing archives might break.

`MTLModel` automatically saves the version of the model object that was used for
archival. When unarchiving, `-decodeValueForKey:withCoder:modelVersion:` will
be invoked if overridden, giving you a convenient hook to upgrade old data.

## MTLJSONSerializing

In order to serialize your model objects from or into JSON, you need to
implement `<MTLJSONSerializing>` in your `MTLModel` subclass. This allows you to
use `MTLJSONAdapter` to convert your model objects from JSON and back:

```objc
NSError *error = nil;
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
```

```objc
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:user];
```

### `+JSONKeyPathsByPropertyKey`

The dictionary returned by this method specifies how your model object's
properties map to the keys in the JSON representation. Properties that map to
`NSNull` will not be present in the JSON representation, for example:

```objc

@interface XYUser : MTLModel

@property (readonly, nonatomic, copy) NSString *name;
@property (readonly, nonatomic, strong) NSDate *createdAt;

@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
@property (readonly, nonatomic, strong) XYHelper *helper;

@end

@implementation XYUser

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{
        @"createdAt": @"created_at",
        @"meUser": NSNull.null
    };
}

- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
    self = [super initWithDictionary:dictionaryValue error:error];
    if (self == nil) return nil;

    _helper = [XYHelper helperWithName:self.name createdAt:self.createdAt];

    return self;
}

@end
```

In this example, the `XYUser` class declares four properties that Mantle
handles in different ways:

- `name` is implicitly mapped to a key of the same name in the JSON
  representation.
- `createdAt` is converted to its snake case equivalent.
- `meUser` is not serialized into JSON.
- `helper` is initialized exactly once after JSON deserialization.

Use `-[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]` if your
model's superclass also implements `MTLJSONSerializing` to merge their mappings.

When deserializing JSON using
`+[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]`, JSON keys that don't
correspond to a property name or have an explicit mapping are ignored:

```objc
NSDictionary *JSONDictionary = @{
    @"name": @"john",
    @"created_at": @"2013/07/02 16:40:00 +0000",
    @"plan": @"lite"
};

XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
```

Here, the `plan` would be ignored since it neither matches a property name of
`XYUser` nor is it otherwise mapped in `+JSONKeyPathsByPropertyKey`.

### `+JSONTransformerForKey:`

Implement this optional method to convert a property from a different type when
deserializing from JSON.

```
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key {
    if ([key isEqualToString:@"createdAt"]) {
        return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName];
    }

    return nil;
}
```

For added convenience, if you implement `+<key>JSONTransformer`,
`MTLJSONAdapter` will use the result of that method instead. For example, dates
that are commonly represented as strings in JSON can be transformed to `NSDate`s
like so:

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

If the transformer is reversible, it will also be used when serializing the
object into JSON.

### `+classForParsingJSONDictionary:`

If you are implementing a class cluster, implement this optional method to
determine which subclass of your base class should be used when deserializing an
object from JSON.

```objc
@interface XYMessage : MTLModel

@end

@interface XYTextMessage: XYMessage

@property (readonly, nonatomic, copy) NSString *body;

@end

@interface XYPictureMessage : XYMessage

@property (readonly, nonatomic, strong) NSURL *imageURL;

@end

@implementation XYMessage

+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {
    if (JSONDictionary[@"image_url"] != nil) {
        return XYPictureMessage.class;
    }

    if (JSONDictionary[@"body"] != nil) {
        return XYTextMessage.class;
    }

    NSAssert(NO, @"No matching class for the JSON dictionary '%@'.", JSONDictionary);
    return self;
}

@end
```

`MTLJSONAdapter` will then pick the class based on the JSON dictionary you pass
in:

```objc
NSDictionary *textMessage = @{
    @"id": @1,
    @"body": @"Hello World!"
};

NSDictionary *pictureMessage = @{
    @"id": @2,
    @"image_url": @"http://example.com/lolcat.gif"
};

XYTextMessage *messageA = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:textMessage error:NULL];

XYPictureMessage *messageB = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:pictureMessage error:NULL];
```

## Persistence

Mantle doesn't automatically persist your objects for you. However, `MTLModel`
does conform to `<NSCoding>`, so model objects can be archived to disk using
`NSKeyedArchiver`.

If you need something more powerful, or want to avoid keeping your whole model
in memory at once, Core Data may be a better choice.

## System Requirements

Mantle supports OS X 10.7+ and iOS 5.0+.

## Importing Mantle

To add Mantle to your application:

 1. Add the Mantle repository as a submodule of your application's repository.
 1. Run `script/bootstrap` from within the Mantle folder.
 1. Drag and drop `Mantle.xcodeproj` into your application's Xcode project or
    workspace.
 1. On the "Build Phases" tab of your application target, add Mantle to the
    "Link Binary With Libraries" phase.
    * **On iOS**, add `libMantle.a`.
    * **On OS X**, add `Mantle.framework`. Mantle must also be added to any
      "Copy Frameworks" build phase. If you don't already have one, simply add a
      "Copy Files" build phase and target the "Frameworks" destination.
 1. Add `"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include" $(inherited)`
    to the "Header Search Paths" build setting (this is only
    necessary for archive builds, but it has no negative effect otherwise).
 1. **For iOS targets**, add `-ObjC` to the "Other Linker Flags" build setting.
 1. **If you added Mantle to a project (not a workspace)**, you will also need
    to add the appropriate Mantle target to the "Target Dependencies" of your
    application.

If you would prefer to use [CocoaPods](http://cocoapods.org), there are some
[Mantle podspecs](https://github.com/CocoaPods/Specs/tree/master/Specs/Mantle) that
have been generously contributed by third parties.

If you’re instead developing Mantle on its own, use the `Mantle.xcworkspace` file.

## License

Mantle is released under the MIT license. See
[LICENSE.md](https://github.com/github/Mantle/blob/master/LICENSE.md).

## More Info

Have a question? Please [open an issue](https://github.com/Mantle/Mantle/issues/new)!
