Repository files navigation

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 for demonstration. How would one typically represent a GitHub issue in Objective-C?

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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) NSString *title;
@property (nonatomic, copy) NSString *body;
- (id)initWithDictionary:(NSDictionary *)dictionary;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}
- (id)initWithDictionary:(NSDictionary *)dictionary {
self = [selfinit];
if (self == nil) returnnil;
_URL = [NSURLURLWithString:dictionary[@"url"]];
_HTMLURL = [NSURLURLWithString:dictionary[@"html_url"]];
_number = dictionary[@"number"];
if ([dictionary[@"state"] isEqualToString:@"open"]) {
_state = GHIssueStateOpen;
} elseif ([dictionary[@"state"] isEqualToString:@"closed"]) {
_state = GHIssueStateClosed;
}
_title = [dictionary[@"title"] copy];
_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 = [selfinit];
if (self == nil) returnnil;
_URL = [coder decodeObjectForKey:@"URL"];
_HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
_number = [coder decodeObjectForKey:@"number"];
_state = [coder decodeUnsignedIntegerForKey:@"state"];
_title = [coder decodeObjectForKey:@"title"];
_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.URLforKey:@"URL"];
if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURLforKey:@"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.body = self.body;
}
- (NSUInteger)hash {
return self.number.hash;
}
- (BOOL)isEqual:(GHIssue *)issue {
if (![issue isKindOfClass:GHIssue.class]) returnNO;
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:

  • If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.
  • There's no way to update a GHIssue with new data from the server.
  • There's no way to turn a GHIssueback 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. This is what GHIssue looks like inheriting from MTLModel:

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] 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 [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformermtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}
+ (NSValueTransformer *)assigneeJSONTransformer {
return [NSValueTransformermtl_JSONDictionaryTransformerWithModelClass:GHUser.class];
}
+ (NSValueTransformer *)updatedAtJSONTransformer {
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
return [self.dateFormatter dateFromString:str];
} reverseBlock:^(NSDate *date) {
return [self.dateFormatter stringFromDate:date];
}];
}
@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:

If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.

The URL transformer we used (included in Mantle) returns nil if given a nil string.

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 GHIssueback 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:

NSError *error = nil;
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
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:

@interfaceXYUser : MTLModel@property (readonly, nonatomic, copy) NSString *name;
@property (readonly, nonatomic, strong) NSDate *createdAt;
@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
@end@implementationXYUser
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"createdAt": @"created_at",
@"meUser": NSNull.null
};
}
@end

In this example, the XYUser class declares three 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.

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:

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 NSDates like so:

+ (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.

@interfaceXYMessage : MTLModel@end@interfaceXYTextMessage: XYMessage@property (readonly, nonatomic, copy) NSString *body;
@end@interfaceXYPictureMessage : XYMessage@property (readonly, nonatomic, strong) NSURL *imageURL;
@end@implementationXYMessage
+ (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:

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.
  2. Run script/bootstrap from within the Mantle folder.
  3. Drag and drop Mantle.xcodeproj into your application's Xcode project or workspace.
  4. 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.
  5. 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).
  6. For iOS targets, add -ObjC to the "Other Linker Flags" build setting.
  7. 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, there are some Mantle podspecs 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.

More Info

Have a question? Please open an issue!

Mantle also has a chat room on Slack. If you'd like to join, just provide your email address and we'll happily send you an invite!

About

Model framework for Cocoa and Cocoa Touch

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

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 for demonstration. How would one typically represent a GitHub issue in Objective-C?

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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) NSString *title;
@property (nonatomic, copy) NSString *body;
- (id)initWithDictionary:(NSDictionary *)dictionary;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}
- (id)initWithDictionary:(NSDictionary *)dictionary {
self = [selfinit];
if (self == nil) returnnil;
_URL = [NSURLURLWithString:dictionary[@"url"]];
_HTMLURL = [NSURLURLWithString:dictionary[@"html_url"]];
_number = dictionary[@"number"];
if ([dictionary[@"state"] isEqualToString:@"open"]) {
_state = GHIssueStateOpen;
} elseif ([dictionary[@"state"] isEqualToString:@"closed"]) {
_state = GHIssueStateClosed;
}
_title = [dictionary[@"title"] copy];
_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 = [selfinit];
if (self == nil) returnnil;
_URL = [coder decodeObjectForKey:@"URL"];
_HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
_number = [coder decodeObjectForKey:@"number"];
_state = [coder decodeUnsignedIntegerForKey:@"state"];
_title = [coder decodeObjectForKey:@"title"];
_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.URLforKey:@"URL"];
if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURLforKey:@"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.body = self.body;
}
- (NSUInteger)hash {
return self.number.hash;
}
- (BOOL)isEqual:(GHIssue *)issue {
if (![issue isKindOfClass:GHIssue.class]) returnNO;
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:

  • If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.
  • There's no way to update a GHIssue with new data from the server.
  • There's no way to turn a GHIssueback 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. This is what GHIssue looks like inheriting from MTLModel:

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] 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 [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformermtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}
+ (NSValueTransformer *)assigneeJSONTransformer {
return [NSValueTransformermtl_JSONDictionaryTransformerWithModelClass:GHUser.class];
}
+ (NSValueTransformer *)updatedAtJSONTransformer {
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
return [self.dateFormatter dateFromString:str];
} reverseBlock:^(NSDate *date) {
return [self.dateFormatter stringFromDate:date];
}];
}
@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:

If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.

The URL transformer we used (included in Mantle) returns nil if given a nil string.

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 GHIssueback 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:

NSError *error = nil;
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
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:

@interfaceXYUser : MTLModel@property (readonly, nonatomic, copy) NSString *name;
@property (readonly, nonatomic, strong) NSDate *createdAt;
@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
@end@implementationXYUser
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"createdAt": @"created_at",
@"meUser": NSNull.null
};
}
@end

In this example, the XYUser class declares three 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.

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:

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 NSDates like so:

+ (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.

@interfaceXYMessage : MTLModel@end@interfaceXYTextMessage: XYMessage@property (readonly, nonatomic, copy) NSString *body;
@end@interfaceXYPictureMessage : XYMessage@property (readonly, nonatomic, strong) NSURL *imageURL;
@end@implementationXYMessage
+ (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:

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.
  2. Run script/bootstrap from within the Mantle folder.
  3. Drag and drop Mantle.xcodeproj into your application's Xcode project or workspace.
  4. 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.
  5. 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).
  6. For iOS targets, add -ObjC to the "Other Linker Flags" build setting.
  7. 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, there are some Mantle podspecs 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.

More Info

Have a question? Please open an issue!

Mantle also has a chat room on Slack. If you'd like to join, just provide your email address and we'll happily send you an invite!

About

Model framework for Cocoa and Cocoa Touch

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

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 for demonstration. How would one typically represent a GitHub issue in Objective-C?

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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) NSString *title;
@property (nonatomic, copy) NSString *body;
- (id)initWithDictionary:(NSDictionary *)dictionary;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}
- (id)initWithDictionary:(NSDictionary *)dictionary {
self = [selfinit];
if (self == nil) returnnil;
_URL = [NSURLURLWithString:dictionary[@"url"]];
_HTMLURL = [NSURLURLWithString:dictionary[@"html_url"]];
_number = dictionary[@"number"];
if ([dictionary[@"state"] isEqualToString:@"open"]) {
_state = GHIssueStateOpen;
} elseif ([dictionary[@"state"] isEqualToString:@"closed"]) {
_state = GHIssueStateClosed;
}
_title = [dictionary[@"title"] copy];
_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 = [selfinit];
if (self == nil) returnnil;
_URL = [coder decodeObjectForKey:@"URL"];
_HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
_number = [coder decodeObjectForKey:@"number"];
_state = [coder decodeUnsignedIntegerForKey:@"state"];
_title = [coder decodeObjectForKey:@"title"];
_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.URLforKey:@"URL"];
if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURLforKey:@"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.body = self.body;
}
- (NSUInteger)hash {
return self.number.hash;
}
- (BOOL)isEqual:(GHIssue *)issue {
if (![issue isKindOfClass:GHIssue.class]) returnNO;
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:

  • If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.
  • There's no way to update a GHIssue with new data from the server.
  • There's no way to turn a GHIssueback 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. This is what GHIssue looks like inheriting from MTLModel:

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] 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 [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformermtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}
+ (NSValueTransformer *)assigneeJSONTransformer {
return [NSValueTransformermtl_JSONDictionaryTransformerWithModelClass:GHUser.class];
}
+ (NSValueTransformer *)updatedAtJSONTransformer {
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
return [self.dateFormatter dateFromString:str];
} reverseBlock:^(NSDate *date) {
return [self.dateFormatter stringFromDate:date];
}];
}
@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:

If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.

The URL transformer we used (included in Mantle) returns nil if given a nil string.

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 GHIssueback 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:

NSError *error = nil;
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
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:

@interfaceXYUser : MTLModel@property (readonly, nonatomic, copy) NSString *name;
@property (readonly, nonatomic, strong) NSDate *createdAt;
@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
@end@implementationXYUser
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"createdAt": @"created_at",
@"meUser": NSNull.null
};
}
@end

In this example, the XYUser class declares three 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.

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:

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 NSDates like so:

+ (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.

@interfaceXYMessage : MTLModel@end@interfaceXYTextMessage: XYMessage@property (readonly, nonatomic, copy) NSString *body;
@end@interfaceXYPictureMessage : XYMessage@property (readonly, nonatomic, strong) NSURL *imageURL;
@end@implementationXYMessage
+ (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:

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.
  2. Run script/bootstrap from within the Mantle folder.
  3. Drag and drop Mantle.xcodeproj into your application's Xcode project or workspace.
  4. 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.
  5. 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).
  6. For iOS targets, add -ObjC to the "Other Linker Flags" build setting.
  7. 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, there are some Mantle podspecs 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.

More Info

Have a question? Please open an issue!

Mantle also has a chat room on Slack. If you'd like to join, just provide your email address and we'll happily send you an invite!

About

Model framework for Cocoa and Cocoa Touch

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

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 for demonstration. How would one typically represent a GitHub issue in Objective-C?

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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) NSString *title;
@property (nonatomic, copy) NSString *body;
- (id)initWithDictionary:(NSDictionary *)dictionary;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}
- (id)initWithDictionary:(NSDictionary *)dictionary {
self = [selfinit];
if (self == nil) returnnil;
_URL = [NSURLURLWithString:dictionary[@"url"]];
_HTMLURL = [NSURLURLWithString:dictionary[@"html_url"]];
_number = dictionary[@"number"];
if ([dictionary[@"state"] isEqualToString:@"open"]) {
_state = GHIssueStateOpen;
} elseif ([dictionary[@"state"] isEqualToString:@"closed"]) {
_state = GHIssueStateClosed;
}
_title = [dictionary[@"title"] copy];
_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 = [selfinit];
if (self == nil) returnnil;
_URL = [coder decodeObjectForKey:@"URL"];
_HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
_number = [coder decodeObjectForKey:@"number"];
_state = [coder decodeUnsignedIntegerForKey:@"state"];
_title = [coder decodeObjectForKey:@"title"];
_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.URLforKey:@"URL"];
if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURLforKey:@"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.body = self.body;
}
- (NSUInteger)hash {
return self.number.hash;
}
- (BOOL)isEqual:(GHIssue *)issue {
if (![issue isKindOfClass:GHIssue.class]) returnNO;
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:

  • If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.
  • There's no way to update a GHIssue with new data from the server.
  • There's no way to turn a GHIssueback 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. This is what GHIssue looks like inheriting from MTLModel:

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] 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 [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformermtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}
+ (NSValueTransformer *)assigneeJSONTransformer {
return [NSValueTransformermtl_JSONDictionaryTransformerWithModelClass:GHUser.class];
}
+ (NSValueTransformer *)updatedAtJSONTransformer {
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
return [self.dateFormatter dateFromString:str];
} reverseBlock:^(NSDate *date) {
return [self.dateFormatter stringFromDate:date];
}];
}
@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:

If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.

The URL transformer we used (included in Mantle) returns nil if given a nil string.

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 GHIssueback 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:

NSError *error = nil;
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
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:

@interfaceXYUser : MTLModel@property (readonly, nonatomic, copy) NSString *name;
@property (readonly, nonatomic, strong) NSDate *createdAt;
@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
@end@implementationXYUser
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"createdAt": @"created_at",
@"meUser": NSNull.null
};
}
@end

In this example, the XYUser class declares three 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.

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:

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 NSDates like so:

+ (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.

@interfaceXYMessage : MTLModel@end@interfaceXYTextMessage: XYMessage@property (readonly, nonatomic, copy) NSString *body;
@end@interfaceXYPictureMessage : XYMessage@property (readonly, nonatomic, strong) NSURL *imageURL;
@end@implementationXYMessage
+ (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:

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.
  2. Run script/bootstrap from within the Mantle folder.
  3. Drag and drop Mantle.xcodeproj into your application's Xcode project or workspace.
  4. 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.
  5. 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).
  6. For iOS targets, add -ObjC to the "Other Linker Flags" build setting.
  7. 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, there are some Mantle podspecs 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.

More Info

Have a question? Please open an issue!

Mantle also has a chat room on Slack. If you'd like to join, just provide your email address and we'll happily send you an invite!

About

Model framework for Cocoa and Cocoa Touch

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

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 for demonstration. How would one typically represent a GitHub issue in Objective-C?

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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) NSString *title;
@property (nonatomic, copy) NSString *body;
- (id)initWithDictionary:(NSDictionary *)dictionary;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}
- (id)initWithDictionary:(NSDictionary *)dictionary {
self = [selfinit];
if (self == nil) returnnil;
_URL = [NSURLURLWithString:dictionary[@"url"]];
_HTMLURL = [NSURLURLWithString:dictionary[@"html_url"]];
_number = dictionary[@"number"];
if ([dictionary[@"state"] isEqualToString:@"open"]) {
_state = GHIssueStateOpen;
} elseif ([dictionary[@"state"] isEqualToString:@"closed"]) {
_state = GHIssueStateClosed;
}
_title = [dictionary[@"title"] copy];
_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 = [selfinit];
if (self == nil) returnnil;
_URL = [coder decodeObjectForKey:@"URL"];
_HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
_number = [coder decodeObjectForKey:@"number"];
_state = [coder decodeUnsignedIntegerForKey:@"state"];
_title = [coder decodeObjectForKey:@"title"];
_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.URLforKey:@"URL"];
if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURLforKey:@"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.body = self.body;
}
- (NSUInteger)hash {
return self.number.hash;
}
- (BOOL)isEqual:(GHIssue *)issue {
if (![issue isKindOfClass:GHIssue.class]) returnNO;
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:

  • If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.
  • There's no way to update a GHIssue with new data from the server.
  • There's no way to turn a GHIssueback 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. This is what GHIssue looks like inheriting from MTLModel:

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] 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 [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformermtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}
+ (NSValueTransformer *)assigneeJSONTransformer {
return [NSValueTransformermtl_JSONDictionaryTransformerWithModelClass:GHUser.class];
}
+ (NSValueTransformer *)updatedAtJSONTransformer {
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
return [self.dateFormatter dateFromString:str];
} reverseBlock:^(NSDate *date) {
return [self.dateFormatter stringFromDate:date];
}];
}
@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:

If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.

The URL transformer we used (included in Mantle) returns nil if given a nil string.

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 GHIssueback 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:

NSError *error = nil;
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
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:

@interfaceXYUser : MTLModel@property (readonly, nonatomic, copy) NSString *name;
@property (readonly, nonatomic, strong) NSDate *createdAt;
@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
@end@implementationXYUser
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"createdAt": @"created_at",
@"meUser": NSNull.null
};
}
@end

In this example, the XYUser class declares three 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.

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:

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 NSDates like so:

+ (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.

@interfaceXYMessage : MTLModel@end@interfaceXYTextMessage: XYMessage@property (readonly, nonatomic, copy) NSString *body;
@end@interfaceXYPictureMessage : XYMessage@property (readonly, nonatomic, strong) NSURL *imageURL;
@end@implementationXYMessage
+ (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:

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.
  2. Run script/bootstrap from within the Mantle folder.
  3. Drag and drop Mantle.xcodeproj into your application's Xcode project or workspace.
  4. 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.
  5. 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).
  6. For iOS targets, add -ObjC to the "Other Linker Flags" build setting.
  7. 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, there are some Mantle podspecs 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.

More Info

Have a question? Please open an issue!

Mantle also has a chat room on Slack. If you'd like to join, just provide your email address and we'll happily send you an invite!

About

Model framework for Cocoa and Cocoa Touch

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

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 for demonstration. How would one typically represent a GitHub issue in Objective-C?

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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) NSString *title;
@property (nonatomic, copy) NSString *body;
- (id)initWithDictionary:(NSDictionary *)dictionary;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}
- (id)initWithDictionary:(NSDictionary *)dictionary {
self = [selfinit];
if (self == nil) returnnil;
_URL = [NSURLURLWithString:dictionary[@"url"]];
_HTMLURL = [NSURLURLWithString:dictionary[@"html_url"]];
_number = dictionary[@"number"];
if ([dictionary[@"state"] isEqualToString:@"open"]) {
_state = GHIssueStateOpen;
} elseif ([dictionary[@"state"] isEqualToString:@"closed"]) {
_state = GHIssueStateClosed;
}
_title = [dictionary[@"title"] copy];
_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 = [selfinit];
if (self == nil) returnnil;
_URL = [coder decodeObjectForKey:@"URL"];
_HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
_number = [coder decodeObjectForKey:@"number"];
_state = [coder decodeUnsignedIntegerForKey:@"state"];
_title = [coder decodeObjectForKey:@"title"];
_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.URLforKey:@"URL"];
if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURLforKey:@"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.body = self.body;
}
- (NSUInteger)hash {
return self.number.hash;
}
- (BOOL)isEqual:(GHIssue *)issue {
if (![issue isKindOfClass:GHIssue.class]) returnNO;
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:

  • If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.
  • There's no way to update a GHIssue with new data from the server.
  • There's no way to turn a GHIssueback 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. This is what GHIssue looks like inheriting from MTLModel:

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] 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 [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformermtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}
+ (NSValueTransformer *)assigneeJSONTransformer {
return [NSValueTransformermtl_JSONDictionaryTransformerWithModelClass:GHUser.class];
}
+ (NSValueTransformer *)updatedAtJSONTransformer {
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
return [self.dateFormatter dateFromString:str];
} reverseBlock:^(NSDate *date) {
return [self.dateFormatter stringFromDate:date];
}];
}
@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:

If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.

The URL transformer we used (included in Mantle) returns nil if given a nil string.

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 GHIssueback 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:

NSError *error = nil;
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
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:

@interfaceXYUser : MTLModel@property (readonly, nonatomic, copy) NSString *name;
@property (readonly, nonatomic, strong) NSDate *createdAt;
@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
@end@implementationXYUser
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"createdAt": @"created_at",
@"meUser": NSNull.null
};
}
@end

In this example, the XYUser class declares three 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.

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:

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 NSDates like so:

+ (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.

@interfaceXYMessage : MTLModel@end@interfaceXYTextMessage: XYMessage@property (readonly, nonatomic, copy) NSString *body;
@end@interfaceXYPictureMessage : XYMessage@property (readonly, nonatomic, strong) NSURL *imageURL;
@end@implementationXYMessage
+ (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:

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.
  2. Run script/bootstrap from within the Mantle folder.
  3. Drag and drop Mantle.xcodeproj into your application's Xcode project or workspace.
  4. 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.
  5. 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).
  6. For iOS targets, add -ObjC to the "Other Linker Flags" build setting.
  7. 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, there are some Mantle podspecs 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.

More Info

Have a question? Please open an issue!

Mantle also has a chat room on Slack. If you'd like to join, just provide your email address and we'll happily send you an invite!

About

Model framework for Cocoa and Cocoa Touch

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

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 for demonstration. How would one typically represent a GitHub issue in Objective-C?

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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) NSString *title;
@property (nonatomic, copy) NSString *body;
- (id)initWithDictionary:(NSDictionary *)dictionary;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}
- (id)initWithDictionary:(NSDictionary *)dictionary {
self = [selfinit];
if (self == nil) returnnil;
_URL = [NSURLURLWithString:dictionary[@"url"]];
_HTMLURL = [NSURLURLWithString:dictionary[@"html_url"]];
_number = dictionary[@"number"];
if ([dictionary[@"state"] isEqualToString:@"open"]) {
_state = GHIssueStateOpen;
} elseif ([dictionary[@"state"] isEqualToString:@"closed"]) {
_state = GHIssueStateClosed;
}
_title = [dictionary[@"title"] copy];
_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 = [selfinit];
if (self == nil) returnnil;
_URL = [coder decodeObjectForKey:@"URL"];
_HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
_number = [coder decodeObjectForKey:@"number"];
_state = [coder decodeUnsignedIntegerForKey:@"state"];
_title = [coder decodeObjectForKey:@"title"];
_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.URLforKey:@"URL"];
if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURLforKey:@"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.body = self.body;
}
- (NSUInteger)hash {
return self.number.hash;
}
- (BOOL)isEqual:(GHIssue *)issue {
if (![issue isKindOfClass:GHIssue.class]) returnNO;
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:

  • If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.
  • There's no way to update a GHIssue with new data from the server.
  • There's no way to turn a GHIssueback 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. This is what GHIssue looks like inheriting from MTLModel:

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] 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 [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformermtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}
+ (NSValueTransformer *)assigneeJSONTransformer {
return [NSValueTransformermtl_JSONDictionaryTransformerWithModelClass:GHUser.class];
}
+ (NSValueTransformer *)updatedAtJSONTransformer {
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
return [self.dateFormatter dateFromString:str];
} reverseBlock:^(NSDate *date) {
return [self.dateFormatter stringFromDate:date];
}];
}
@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:

If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.

The URL transformer we used (included in Mantle) returns nil if given a nil string.

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 GHIssueback 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:

NSError *error = nil;
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
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:

@interfaceXYUser : MTLModel@property (readonly, nonatomic, copy) NSString *name;
@property (readonly, nonatomic, strong) NSDate *createdAt;
@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
@end@implementationXYUser
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"createdAt": @"created_at",
@"meUser": NSNull.null
};
}
@end

In this example, the XYUser class declares three 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.

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:

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 NSDates like so:

+ (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.

@interfaceXYMessage : MTLModel@end@interfaceXYTextMessage: XYMessage@property (readonly, nonatomic, copy) NSString *body;
@end@interfaceXYPictureMessage : XYMessage@property (readonly, nonatomic, strong) NSURL *imageURL;
@end@implementationXYMessage
+ (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:

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.
  2. Run script/bootstrap from within the Mantle folder.
  3. Drag and drop Mantle.xcodeproj into your application's Xcode project or workspace.
  4. 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.
  5. 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).
  6. For iOS targets, add -ObjC to the "Other Linker Flags" build setting.
  7. 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, there are some Mantle podspecs 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.

More Info

Have a question? Please open an issue!

Mantle also has a chat room on Slack. If you'd like to join, just provide your email address and we'll happily send you an invite!

About

Model framework for Cocoa and Cocoa Touch

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

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 for demonstration. How would one typically represent a GitHub issue in Objective-C?

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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) NSString *title;
@property (nonatomic, copy) NSString *body;
- (id)initWithDictionary:(NSDictionary *)dictionary;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}
- (id)initWithDictionary:(NSDictionary *)dictionary {
self = [selfinit];
if (self == nil) returnnil;
_URL = [NSURLURLWithString:dictionary[@"url"]];
_HTMLURL = [NSURLURLWithString:dictionary[@"html_url"]];
_number = dictionary[@"number"];
if ([dictionary[@"state"] isEqualToString:@"open"]) {
_state = GHIssueStateOpen;
} elseif ([dictionary[@"state"] isEqualToString:@"closed"]) {
_state = GHIssueStateClosed;
}
_title = [dictionary[@"title"] copy];
_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 = [selfinit];
if (self == nil) returnnil;
_URL = [coder decodeObjectForKey:@"URL"];
_HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
_number = [coder decodeObjectForKey:@"number"];
_state = [coder decodeUnsignedIntegerForKey:@"state"];
_title = [coder decodeObjectForKey:@"title"];
_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.URLforKey:@"URL"];
if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURLforKey:@"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.body = self.body;
}
- (NSUInteger)hash {
return self.number.hash;
}
- (BOOL)isEqual:(GHIssue *)issue {
if (![issue isKindOfClass:GHIssue.class]) returnNO;
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:

  • If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.
  • There's no way to update a GHIssue with new data from the server.
  • There's no way to turn a GHIssueback 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. This is what GHIssue looks like inheriting from MTLModel:

typedefenum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interfaceGHIssue : 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;
@end
@implementationGHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatteralloc] init];
dateFormatter.locale = [[NSLocalealloc] 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 [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformervalueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformermtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}
+ (NSValueTransformer *)assigneeJSONTransformer {
return [NSValueTransformermtl_JSONDictionaryTransformerWithModelClass:GHUser.class];
}
+ (NSValueTransformer *)updatedAtJSONTransformer {
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
return [self.dateFormatter dateFromString:str];
} reverseBlock:^(NSDate *date) {
return [self.dateFormatter stringFromDate:date];
}];
}
@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:

If the url or html_url field is missing, +[NSURL URLWithString:] will throw an exception.

The URL transformer we used (included in Mantle) returns nil if given a nil string.

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 GHIssueback 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:

NSError *error = nil;
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
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:

@interfaceXYUser : MTLModel@property (readonly, nonatomic, copy) NSString *name;
@property (readonly, nonatomic, strong) NSDate *createdAt;
@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
@end@implementationXYUser
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"createdAt": @"created_at",
@"meUser": NSNull.null
};
}
@end

In this example, the XYUser class declares three 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.

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:

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 NSDates like so:

+ (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.

@interfaceXYMessage : MTLModel@end@interfaceXYTextMessage: XYMessage@property (readonly, nonatomic, copy) NSString *body;
@end@interfaceXYPictureMessage : XYMessage@property (readonly, nonatomic, strong) NSURL *imageURL;
@end@implementationXYMessage
+ (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:

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.
  2. Run script/bootstrap from within the Mantle folder.
  3. Drag and drop Mantle.xcodeproj into your application's Xcode project or workspace.
  4. 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.
  5. 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).
  6. For iOS targets, add -ObjC to the "Other Linker Flags" build setting.
  7. 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, there are some Mantle podspecs 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.

More Info

Have a question? Please open an issue!

Mantle also has a chat room on Slack. If you'd like to join, just provide your email address and we'll happily send you an invite!

About

Model framework for Cocoa and Cocoa Touch

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors