Mantle is a model framework for iOS that provides a convenient way to create objects from JSON and transform those objects back into JSON. This is especially useful when dealing with a remote API.
We are going to take a look at MTLModel
, MTLJSONAdapter
and why you might want to consider using Mantle for your next project.
MTLModel
MTLModel
provides an easy way to map NSDictionary
objects to Objective-C classes and vice-versa.
To get started, lets look at a simple example. Assume we get the following JSON response from our API endpoint and want to create and populate our (yet to be created) CATProfile
model class.
{
"id": 1,
"name": "Objective Cat",
"birthday": "2013-09-12 13:29:36 +0100",
"website": "http://objc.at",
"location": { "lat": "48.2083", "lon": "16.3731" },
"relationship_status": "single",
"awesome": true
}
Lets create a new MTLModel
subclass that represents the JSON object above.
// CATProfile.h
typedef NS_ENUM(NSInteger, CATRelationshipStatus) {
CATRelationshipStatusSingle = 0,
CATRelationshipStatusInRelationship,
CATRelationshipStatusComplicated
};
@interface CATProfile : MTLModel<MTLJSONSerializing>
@property(strong, nonatomic) NSNumber *profileId;
@property(strong, nonatomic) NSString *name;
@property(strong, nonatomic) NSDate *birthday;
@property(strong, nonatomic) NSURL *website;
@property(nonatomic) CLLocationCoordinate2D locationCoordinate;
@property(nonatomic) CATRelationshipStatus relationshipStatus;
@property(nonatomic, getter=isAwesome) BOOL awesome;
@end
The CATProfile
class inherits from MTLModel
and implements the MTLJSONSerializing
protocol. The protocol requires us to implement +JSONKeyPathsByPropertyKey
.
// CATProfile.m
@implementation
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
// properties defined in header < : > key in JSON Dictionary
return @{
@"profileId": @"id",
@"websiteURL": @"website",
@"locationCoordinate": @"location",
@"relationshipStatus": @"relationship_status",
};
}
@end
+JSONKeyPathsByPropertyKey
returns an NSDictionary with key-value pairs of each model property that should be matched to a value in JSON. This makes sure Mantle knows which JSON key to use to populate a specific model property.
Obiously absent from this list are the name
, birthday
and awesome
properties. If a property is ommited from the dictionary, Mantle will automatically look for a JSON key with the same name as the property defined in the header.
NSValueTransformer
Mantle can handle the conversion of arbitrary types such as NSString
and NSNumber
by default. However, it needs some help with non-arbitrary types such as NSURL
and enums as well as custom structs like CLLocationCoordinate2D
.
Mantle relies on the help of Foundation's NSValueTransformer
to map values between the JSON representation of the model to the actual properties on the Objective-C object.
To create a custom transformer for a model property, we need to implement a class method called +<propertyName>JSONTransformer
and return the desired NSValueTransformer
.
// mapping birthday to NSDate and vice-versa
+ (NSValueTransformer *)birthdayJSONTransformer {
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *dateString) {
return [self.dateFormatter dateFromString:dateString];
} reverseBlock:^(NSDate *date) {
return [self.dateFormatter stringFromDate:date];
}];
}
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
return dateFormatter;
}
Mantle calls this method at runtime to determine how to transform the birthday property. The forward block transforms a string to an NSDate
object and the reverse block takes the NSDate
object and converts it back to a string. Nice!
For reference, here is a list of transformer methods for all the other non-arbitrary properties that need our attention.
NSURL ↔︎ JSON string
+ (NSValueTransformer *)websiteURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
CLLocationCoordinate2D ↔︎ JSON object
+ (NSValueTransformer *)locationCoordinateJSONTransformer {
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSDictionary *coordinateDict) {
CLLocationDegrees latitude = [coordinateDict[@"lat"] doubleValue];
CLLocationDegrees longitude = [coordinateDict[@"lon"] doubleValue];
return [NSValue valueWithMKCoordinate:CLLocationCoordinate2DMake(latitude, longitude)];
} reverseBlock:^(NSValue *coordinateValue) {
CLLocationCoordinate2D coordinate = [coordinateValue MKCoordinateValue];
return @{@"lat": @(coordinate.latitude), @"lon": @(coordinate.longitude)};
}];
}
enum ↔︎ JSON string
+ (NSValueTransformer *)relationshipStatusJSONTransformer {
return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
@"single": @(CATRelationshipStatusSingle),
@"relationship": @(CATRelationshipStatusInRelationship),
@"complicated": @(CATRelationshipStatusComplicated)
}];
}
BOOL ↔︎ JSON boolean
+ (NSValueTransformer *)awesomeJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName];
}
Create model objects from JSON
As soon as the model is fully configured, its time to get the JSON data from the API and convert it to an instance of our model. First, we need to convert the JSON representation to an NSDictionary
, that can be used by Mantle to create our model. Luckily iOS provides a great way to do that via NSJSONSerialization
.
After that, the MTLJSONAdapter
class that ships with Mantle does the heavy-lifting to create our model.
// create NSDictionary from JSON data
NSData JSONData = ... // the JSON response from the API
NSDictionary *JSONDict = [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:NULL];
// create model object from NSDictionary using MTLJSONSerialisation
CATProfile *profile = [MTLJSONAdapter modelOfClass:CATProfile.class fromJSONDictionary:JSONDict error:NULL];
Create JSON from model objects
MTLJSONAdapter
is als capable of creating an NSDictionary
from our model class that can be direcly encoded back into a JSON string.
// create NSDictionary from model class using MTLJSONSerialisation
CATProfile *profile = ...
NSDictionary *profileDict = [MTLJSONAdapter JSONDictionaryFromModel:profile];
// convert NSDictionary to JSON data
NSData *JSONData = [NSJSONSerialization dataWithJSONObject:profileDict options:0 error:NULL];
Note: If you have a property on your model that should not be included when creating a JSON represenatation of your model, return
NSNull.null
. E.g.@{"name": NSNull.null}
in+JSONKeyPathsByPropertyKey
. Mantle will savely ignore this property.
Mapping Arrays and Dictionaries
Most of the time, models have relationships to other models. These relationships are commonly represented via JSON arrays or objects (e.g. owner and friends).
{
"id": 1,
"name": "Objective Cat",
...,
"owner": {
"id": 99,
"name": "Alexander Schuch"
},
"friends": [
{
"name": "Owly",
"type": "bird"
},
{
"name": "Hedgy",
"type": "mammal"
}
]
}
Mantle supports mapping these relationships to new models out of the box. In order to make sure Mantle knows how to transform the relationships we can use one of the following provided category methods on NSValueTransformer
.
+ (NSValueTransformer *)mtl_JSONDictionaryTransformerWithModelClass:(Class)modelClass;
+ (NSValueTransformer *)mtl_JSONArrayTransformerWithModelClass:(Class)modelClass;
Of course Mantle needs to know about these relationships and their MTLModel
subclasses they should be transformed to. Its as easy as creating new MTLModel
subclasses and implementing the MTLJSONSerializing
protocol for the objects that should be mapped. Then we can add some new properties to our CATProfile
class and implement two new transformers.
// CATProfile.h
@property(strong, nonatomic) CATOwner *owner; // CATOwner is a MTLModel subclass
@property(strong, nonatomic) NSArray *friends; // Array of CATFriend objects
// CATProfile.m
+ (NSValueTransformer *)ownerJSONTransformer {
return [NSValueTransformer mtl_JSONDictionaryTransformerWithModelClass:CATOwner.class];
}
+ (NSValueTransformer *)friendsJSONTransformer {
return [NSValueTransformer mtl_JSONArrayTransformerWithModelClass:CATFriend.class];
}
Some nice additions
We briefly talked about NSValueTransformer
before. NSValueTransformer
has the nice feature that makes it possible to globally register a transformer by its name. In case you are using the same transformers all over your app, make sure to subclass NSValueTransformer
, register your custom transformer once and subsequently use it in your MTLModel
s.
// In CATProfile.m
NSString * const kCATCustomValueTransformerName = @"CATCustomValueTransformer";
+ (void)initialize
{
// Register NSValueTransformer
if (self == CATProfile.class) {
CATCustomValueTransformer *transformer = [CATCustomValueTransformer new];
[NSValueTransformer setValueTransformer:transformer forName:kCATCustomValueTransformerName];
}
}
// Then use the custom transformer to translate properties using Mantle
+ (NSValueTransformer *)whateverPropertyJSONTransformer {
return [NSValueTransformer valueTransformerForName:kCATCustomValueTransformerName];
}
Conclusion
Mantle is a nice addition when working with JSON APIs. However, be aware that it might not be suitable for you if you have to deal with very complex and erratic APIs.
Try Mantle in your next project and let me know what you use it for on Twitter.