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?
typedef enum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interface GHIssue : NSObject <NSCoding, NSCopying>
@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, copy, readonly) NSDate *updatedAt;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *retrievedAt;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;
- (id)initWithDictionary:(NSDictionary *)dictionary;
@end
@implementation GHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}
- (id)initWithDictionary:(NSDictionary *)dictionary {
self = [self init];
if (self == nil) return nil;
_URL = [NSURL URLWithString:dictionary[@"url"]];
_HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]];
_number = dictionary[@"number"];
if ([dictionary[@"state"] isEqualToString:@"open"]) {
_state = GHIssueStateOpen;
} else if ([dictionary[@"state"] isEqualToString:@"closed"]) {
_state = GHIssueStateClosed;
}
_title = [dictionary[@"title"] copy];
_retrievedAt = [NSDate date];
_body = [dictionary[@"body"] copy];
_reporterLogin = [dictionary[@"user"][@"login"] copy];
_assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]];
_updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];
return self;
}
- (id)initWithCoder:(NSCoder *)coder {
self = [self init];
if (self == nil) return nil;
_URL = [coder decodeObjectForKey:@"URL"];
_HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
_number = [coder decodeObjectForKey:@"number"];
_state = [coder decodeUnsignedIntegerForKey:@"state"];
_title = [coder decodeObjectForKey:@"title"];
_retrievedAt = [NSDate date];
_body = [coder decodeObjectForKey:@"body"];
_reporterLogin = [coder decodeObjectForKey:@"reporterLogin"];
_assignee = [coder decodeObjectForKey:@"assignee"];
_updatedAt = [coder decodeObjectForKey:@"updatedAt"];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
if (self.URL != nil) [coder encodeObject:self.URL forKey:@"URL"];
if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURL forKey:@"HTMLURL"];
if (self.number != nil) [coder encodeObject:self.number forKey:@"number"];
if (self.title != nil) [coder encodeObject:self.title forKey:@"title"];
if (self.body != nil) [coder encodeObject:self.body forKey:@"body"];
if (self.reporterLogin != nil) [coder encodeObject:self.reporterLogin forKey:@"reporterLogin"];
if (self.assignee != nil) [coder encodeObject:self.assignee forKey:@"assignee"];
if (self.updatedAt != nil) [coder encodeObject:self.updatedAt forKey:@"updatedAt"];
[coder encodeUnsignedInteger:self.state forKey:@"state"];
}
- (id)copyWithZone:(NSZone *)zone {
GHIssue *issue = [[self.class allocWithZone:zone] init];
issue->_URL = self.URL;
issue->_HTMLURL = self.HTMLURL;
issue->_number = self.number;
issue->_state = self.state;
issue->_reporterLogin = self.reporterLogin;
issue->_assignee = self.assignee;
issue->_updatedAt = self.updatedAt;
issue.title = self.title;
issue->_retrievedAt = [NSDate date];
issue.body = self.body;
return issue;
}
- (NSUInteger)hash {
return self.number.hash;
}
- (BOOL)isEqual:(GHIssue *)issue {
if (![issue isKindOfClass:GHIssue.class]) return NO;
return [self.number isEqual:issue.number] && [self.title isEqual:issue.title] && [self.body isEqual:issue.body];
}
@end
Whew, that's a lot of boilerplate for something so simple! And, even then, there are some problems that this example doesn't address:
- There's no way to update a
GHIssue
with new data from the server. - There's no way to turn a
GHIssue
back into JSON. GHIssueState
shouldn't be encoded as-is. If the enum changes in the future, existing archives might break.- If the interface of
GHIssue
changes down the road, existing archives might break.
Why Not Use Core Data?
Core Data solves certain problems very well. If you need to execute complex queries across your data, handle a huge object graph with lots of relationships, or support undo and redo, Core Data is an excellent fit.
It does, however, come with a couple of pain points:
- There's still a lot of boilerplate. Managed objects reduce some of the boilerplate seen above, but Core Data has plenty of its own. Correctly setting up a Core Data stack (with a persistent store and persistent store coordinator) and executing fetches can take many lines of code.
- It's hard to get right. Even experienced developers can make mistakes when using Core Data, and the framework is not forgiving.
If you're just trying to access some JSON objects, Core Data can be a lot of work for little gain.
Nonetheless, if you're using or want to use Core Data in your app already, Mantle can still be a convenient translation layer between the API and your managed model objects.
MTLModel
Enter
MTLModel.
This is what GHIssue
looks like inheriting from MTLModel
:
typedef enum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interface GHIssue : MTLModel <MTLJSONSerializing>
@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *updatedAt;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;
@property (nonatomic, copy, readonly) NSDate *retrievedAt;
@end
@implementation GHIssue
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"URL": @"url",
@"HTMLURL": @"html_url",
@"number": @"number",
@"state": @"state",
@"reporterLogin": @"user.login",
@"assignee": @"assignee",
@"updatedAt": @"updated_at"
};
}
+ (NSValueTransformer *)URLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}
+ (NSValueTransformer *)assigneeJSONTransformer {
return [MTLJSONAdapter dictionaryTransformerWithModelClass:GHUser.class];
}
+ (NSValueTransformer *)updatedAtJSONTransformer {
return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {
return [self.dateFormatter dateFromString:dateString];
} reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {
return [self.dateFormatter stringFromDate:date];
}];
}
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
self = [super initWithDictionary:dictionaryValue error:error];
if (self == nil) return nil;
// Store a value that needs to be determined locally upon initialization.
_retrievedAt = [NSDate date];
return self;
}
@end
Notably absent from this version are implementations of <NSCoding>
,
<NSCopying>
, -isEqual:
, and -hash
. By inspecting the @property
declarations you have in your subclass, MTLModel
can provide default
implementations for all these methods.
The problems with the original example all happen to be fixed as well:
There's no way to update a
GHIssue
with new data from the server.
MTLModel
has an extensible -mergeValuesForKeysFromModel:
method, which makes
it easy to specify how new model data should be integrated.
There's no way to turn a
GHIssue
back into JSON.
This is where reversible transformers really come in handy. +[MTLJSONAdapter JSONDictionaryFromModel:error:]
can transform any model object conforming to
<MTLJSONSerializing>
back into a JSON dictionary. +[MTLJSONAdapter JSONArrayFromModels:error:]
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];
NSError *error = nil;
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:user error:&error];
+JSONKeyPathsByPropertyKey
The dictionary returned by this method specifies how your model object's properties map to the keys in the JSON representation, for example:
@interface XYUser : MTLModel
@property (readonly, nonatomic, copy) NSString *name;
@property (readonly, nonatomic, strong) NSDate *createdAt;
@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
@property (readonly, nonatomic, strong) XYHelper *helper;
@end
@implementation XYUser
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"name": @"name",
@"createdAt": @"created_at"
};
}
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
self = [super initWithDictionary:dictionaryValue error:error];
if (self == nil) return nil;
_helper = [XYHelper helperWithName:self.name createdAt:self.createdAt];
return self;
}
@end
In this example, the XYUser
class declares four properties that Mantle
handles in different ways:
name
is mapped to a key of the same name in the JSON representation.createdAt
is converted to its snake case equivalent.meUser
is not serialized into JSON.helper
is initialized exactly once after JSON deserialization.
Use -[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]
if your
model's superclass also implements MTLJSONSerializing
to merge their mappings.
If you'd like to map all properties of a Model class to themselves, you can use
the +[NSDictionary mtl_identityPropertyMapWithModel:]
helper method.
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;
}
key
is the key that applies to your model object; not the original JSON key. Keep this in mind if you transform the key names using +JSONKeyPathsByPropertyKey
.
For added convenience, if you implement +<key>JSONTransformer
,
MTLJSONAdapter
will use the result of that method instead. For example, dates
that are commonly represented as strings in JSON can be transformed to NSDate
s
like so:
return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {
return [self.dateFormatter dateFromString:dateString];
} reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {
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.
@interface XYMessage : MTLModel
@end
@interface XYTextMessage: XYMessage
@property (readonly, nonatomic, copy) NSString *body;
@end
@interface XYPictureMessage : XYMessage
@property (readonly, nonatomic, strong) NSURL *imageURL;
@end
@implementation XYMessage
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {
if (JSONDictionary[@"image_url"] != nil) {
return XYPictureMessage.class;
}
if (JSONDictionary[@"body"] != nil) {
return XYTextMessage.class;
}
NSAssert(NO, @"No matching class for the JSON dictionary '%@'.", JSONDictionary);
return self;
}
@end
MTLJSONAdapter
will then pick the class based on the JSON dictionary you pass
in:
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 the following platform deployment targets:
- macOS 10.10+
- iOS 8.0+
- tvOS 9.0+
- watchOS 2.0+
Importing Mantle
Manually
To add Mantle to your application:
- Add the Mantle repository as a submodule of your application's repository.
- Run
git submodule update --init --recursive
from within the Mantle folder. - Drag and drop
Mantle.xcodeproj
into your application's Xcode project. - On the "General" tab of your application target, add
Mantle.framework
to the "Embedded Binaries".
If you’re instead developing Mantle on its own, use the Mantle.xcworkspace
file.
Carthage
Simply add Mantle to your Cartfile
:
github "Mantle/Mantle"
CocoaPods
Add Mantle to your Podfile
under the build target they want it used in:
target 'MyAppOrFramework' do
pod 'Mantle'
end
Then run a pod install
within Terminal or the CocoaPods app.
Swift Package Manager
If you are writing an application, add Mantle to your project dependencies directly within Xcode.
If you are writing a package that requires Mantle as dependency, add it to the dependencies
list in its Package.swift
manifest, for example:
dependencies: [
.package(url: "https://github.com/Mantle/Mantle.git", .upToNextMajor(from: "2.0.0"))
]
License
Mantle is released under the MIT license. See LICENSE.md.
More Info
Have a question? Please open an issue!
Github
link |
Stars: 11301 |
Related Packages
You may find interesting
Releases
2.1.6: Prefix fix in post - 2020-10-17T19:41:53
- Fixes a regression for CocoaPods users that also depend on libextobjc – #872 courtesy of @defagos.
2.1.5: Tailor Swift to Your Needs - 2020-10-14T08:25:26
- Adds Swift Package Manager support courtesy of @defagos in #870.
2.1.4: It Just Works - 2020-09-26T12:02:48
- Binary releases should now be made available to Carthage users automatically (thanks to @3a4oT in #867 )
2.1.3: Nothing to see here - 2020-09-22T19:02:33
- Binary releases should now be made available to Carthage users automatically (thanks to @3a4oT in #866),
2.1.2: Time for a recap - 2020-09-18T10:04:28
- Fixed an issue with Xcode 12 #862 – thanks @yhkaplan
2.1.1: Taking up the Mantle - 2020-01-27T21:21:33
- Block definition fix for mtl_cleanupBlock_t (@mogelbuster in #822)
- Suppress documentation warnings for
@encode
(@Konshin in #785) - Xcode 10.2 compatibility (@yhkaplan in #834)
- Spring cleaning of libextobjc (@robb in #844)
- Error added for passing nil JSON dictionary to
-[MTLJSONAdapter modelFromJSONDictionary:error:]
(@hashier in #751) - Cocoapods is now supported officially (@abotkin-cpi in #846)
- GitHub Actions added (@robb in #844 & @dcaunt in #804)
- Carthage updates (@dcaunt in #804 & @alexpezzi in #809)
2.1.0 - 2016-10-12T14:55:40
- watchOS support (@ikesyo in #730)
- tvOS support (@ikesyo in #731)
- Xcode 8 support (@dcaunt in #758)
- Added implicit NSUUID JSON value transformer (@conraddev in #723)
Also includes documentation improvements thanks to @getaaron and @bruun. Thanks to @ikesyo for Xcode/Travis maintenance.
1.5.8: A Spectacular Blast from the Past - 2016-06-10T14:41:31
This release contains the same functionality as 1.5.7 but fixes a warning for CocoaPods users (#488).
2.0.7 - 2016-04-15T14:23:33
MTLJSONAdapter
correctly checks forMTLModel
protocol conformance rather thanMTLModel
inheritance @startupthekid in #617, @priteshshah1983 in #631)- Improved error descriptions when serializing models from JSON fails (@kypselia in #661)
- Add predefined
NSDate
,NSNumber
andNSFormatter
value transformer factory methods (@conradev in #641) - Fix Swift deprecation warnings (@msanders in #680)
- Apply default
MTLJSONAdapter
type transformer for a key when usingJSONTransformerForKey:
if no custom transformer is returned (@k06a in #685)
Thanks to @robb for reviewing pull requests!
1.5.7: A Faster Blast from the Past - 2016-02-24T19:48:50
Backported "faster method calls" (@knox in #679)
1.5.6: Blast from the Past - 2015-10-15T19:50:46
- Renamed
libextobjc
files to avoid collisions. #623
2.0.5 - 2015-09-15T08:22:17
Implicitly transform properties for types conforming to MTLJSONSerializing
(@younthu in #605)
2.0.4: One Infinite Loop - 2015-08-13T08:32:18
Fixed an issue with recursive definitions (@jmah in https://github.com/Mantle/Mantle/pull/582)
2.0.3: Full Glottal Stops - 2015-08-04T13:01:57
- Unmapped, optional protocol properties are now using
MTLStoragePropertyNone
(@kevin-traansmission in #523) MTLJSONAdapter
subclasses now behave correctly with class clusters (@hengw in #556)- Copying a
MTLModel
skips revalidation for better performance (@zadr in #548) - Fewer
MTLJSONAdapter
allocations in class-based transformers (@jmah in #567) - Correctly handle storage behavior for optional properties or those declared in a super class (@jmah in #568)
Also includes improvements to the README by @danielgalasko @alikaragoz :sparkling_heart:
2.0.2 - 2015-05-23T02:53:30
- #525 fixes a memory management crash introduced in #520.
2.0.1 - 2015-05-22T18:02:38
- #489 and #499 fix Objective-C++ compatibility (thanks @zsk425, @johanrydenstam, and @robb!)
- #503 improves error messaging for validating transformers (thanks @robb!)
- #520 dramatically improves the performance of dynamic method invocations (thanks @adamkaplan!)
2.0 - 2015-04-01T16:07:24
This release of Mantle contains major breaking changes that we were unable to make after freezing the 1.0 API. The changes in 2.0 focus on simplifying concepts and increasing flexibility in the framework.
See the CHANGELOG for more information about what’s changed, and how to migrate your project.
2.0-beta.1 - 2015-03-17T20:40:18
This release of Mantle contains major breaking changes that we were unable to make after freezing the 1.0 API. The changes in 2.0 focus on simplifying concepts and increasing flexibility in the framework.
See the CHANGELOG for more information about what’s changed, and how to migrate your project.
1.5.4 - 2015-01-16T21:06:52
Pacify Xcode (#437).
1.5.3 - 2014-12-30T05:09:26
1.5.2 - 2014-11-21T22:36:15
Adds support for Xcode 6 and Carthage.
1.5.1 - 2014-10-07T17:15:39
- Ordered Core Data relationships will now be serialized into
NSOrderedSet
- Illegal mappings in
+JSONKeyPathsByPropertyKey
now throw an exception
With kind contributions by @5teev, @Ahti, @alanrogers, @joshaber, @jspahrsummers, @kylef, @mdiep, @mickeyreiss, @rawrjustin, @robb and @tomtaylor.
1.5 - 2014-05-25T13:20:05
- Adds
Mantle.xcworkspace
for easier development and integration. - Adds convenience methods for serializing and deserializing arrays of
MTLModel
s to JSON. - Adds default values for value mapping transformers.
- If
+JSONKeyPathByPropertyKey
or+managedObjectKeysByPropertyKey
map to property keys not part of+[MTLModel propertyKeys]
, an error is returned. -[MTLModel mergeValuesForKeysFromModel:]
now property handles merging between subclasses.- Prevents associated managed objects from being updated with incomplete data.
With kind contributions by @DAloG, @bartvandendriessche, @robrix, @jspahrsummers, @robb, @paulthorsteinson, @paulyoung and @kilink :sparkling_heart:
1.4.1 - 2014-03-11T17:40:22
- Fixes a regression introduced in
1.4.
which broke access to JSON key paths containing non-dictionary elements. - Adds compatibility with Xcode 5.1
1.4 - 2014-03-01T13:57:26
- Improved error handling
- Support for arm64
- Improved bubbling up of Core Data errors
- Handle model initialization with invalid data-structures
- Expose
-JSONKeyPathForKey:
- Handle illegal key paths
mtl_valueMappingTransformerWithDictionary:
usesNSNull
as default input value- Log class and key when decoding fails
With kind contributions by @dcordero, @keithduncan, @jspahrsummers, @jilouc, @maxgoedjen, @robb, @dcaunt, @dannygreg, @joshaber, @bdolman, @alanjrogers, @ColinEberhardt, @blueless, @dataxpress, @kylef, @eliperkins, @notjosh and @dblock :sparkling_heart: