Post

Mantle

Basic usage of Mantle

Mantle

Basic Usage of Mantle

What is Mantle?

The description on GitHub is:

Model framework for Cocoa and Cocoa Touch

This is a model framework. So what exactly does it do?

Think back to development work: have you often had to discuss model field naming with backend engineers? Should the backend follow your rules, should you follow theirs, or should each side use different names? This is a serialization and deserialization problem. Of course, if field names are consistent, one line of code is enough to convert a dictionary to a model: - (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;. But in real projects, this is almost impossible to achieve. For example, id is a reserved word in Objective-C. Mantle provides a conversion mechanism: it performs serialization and deserialization based on custom property mapping, in other words, field conversion.

How Do You Use It?

Field Mapping

Mantle provides a base class, MTLModel. If you want to use Mantle’s features, the model you create must be a subclass of this class. For example, create a Member class.

Member.h

1
2
3
4
5
@interface Member : MTLModel<MTLJSONSerializing>
@property (nonatomic, retain) NSString *memberID;
@property (nonatomic, retain) NSString *mobilePhone;
@property (nonatomic, retain) NSDate   *createDate;
@property (nonatomic, retain) NSNumber *goldNumber;

The superclass of Member is MTLModel, and it also conforms to the <MTLJSONSerializing> protocol. If you look at this protocol, you will find a required method:

1
+ (NSDictionary *)JSONKeyPathsByPropertyKey;

This is the method used for field mapping mentioned above. Implement it like this:

Member.m

1
2
3
4
5
6
7
8
+ (NSDictionary *)JSONKeyPathsByPropertyKey{
    return @{
             @"memberID" : @"id",
             @"mobilePhone" : @"phone",
             @"createDate" : @"date",
             @"goldNumber" : @"goldNumber"
             };
}

This means the client’s memberID field corresponds to the id field in the data returned by the server. Note: the local field comes first, and the server field comes second. Once this method is implemented, serialization and deserialization will follow this property mapping relationship. Of course, if the key values are the same, there is no need to write the mapping.

Note: in the latest 2.0 version, identical fields can no longer be omitted. In other words, in the dictionary returned by + (NSDictionary *)JSONKeyPathsByPropertyKey, @"goldNumber" : @"goldNumber" must be written. If it is omitted, it is treated as not being serialized, and the value for this field will be empty.

Finally, use one line of code to get the model you want.

1
2
3
4
5
6
7
8
}
NSDictionary *response = @{
                          @"id" : @"1",
                          @"phone" : @"xxxxxxxx",
                          @"date" : @"2014-09-09",
                          @"goldNumber" : @2
                          };
Member *member = [MTLJSONAdapter modelOfClass:[Member class] fromJSONDictionary:response error:nil];

Yes, that completes field conversion. Compared with writing cumbersome if/else statements for field mapping, this is much more convenient.

Note: if the model fields and JSON data match exactly, use

1
2
3
+ (NSDictionary *)JSONKeyPathsByPropertyKey{
    return [NSDictionary mtl_identityPropertyMapWithModel:self];
}

.

Of course, Mantle has more features than the one mentioned above. Before introducing the others, let’s add a few more fields to Member.

1
2
3
4
5
6
7
8
@interface Member : MTLModel<MTLJSONSerializing>
@property (nonatomic, retain) NSString   *memberID;
@property (nonatomic, retain) NSString   *mobilePhone;
@property (nonatomic, retain) NSDate     *createDate;
@property (nonatomic, retain) NSNumber   *goldNumber;
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, assign) BOOL       isVip;
@property (nonatomic, retain) NSURL      *url;

Take the createDate field as an example: if you want to get an NSDate directly in the model, you must convert NSString to NSDate.

Type Conversion

Just like field mapping, you must implement the method in <MTLJSONSerializing>:

1
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;

Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key{
    if ([key isEqualToString:@"createDate"]) {
        return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *string, BOOL *success, NSError *__autoreleasing *error) {
            return [self.dateFormatter dateFromString:string];
        } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {
             return [self.dateFormatter stringFromDate:date];
        }];
    }
    else{
        return nil;
    }
}
+ (NSDateFormatter *)dateFormatter {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.dateFormat = @"yyyy-MM-dd";
    return dateFormatter;
}

API:

1
+ (instancetype)transformerUsingForwardBlock:(MTLValueTransformerBlock)forwardTransformation reverseBlock:(MTLValueTransformerBlock)reverseTransformation;

The value returned by the first block is the result of dictionary -> model conversion, and the value returned by the second block is the result of model -> dictionary conversion. Of course, if we only need serialization, then a one-way conversion is enough. Use the following API:

1
+ (instancetype)transformerUsingForwardBlock:(MTLValueTransformerBlock)transformation;

Mantle also provides another way to implement the same functionality:

1
2
3
4
5
6
7
+ (NSValueTransformer *)createDateJSONTransformer{
	return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *string, BOOL *success, NSError *__autoreleasing *error) {
            return [self.dateFormatter dateFromString:string];
        } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {
             return [self.dateFormatter stringFromDate:date];
        }];
}

The naming rule for these methods is +<key>JSONTransformer. In addition, there are shortcut methods for BOOL and NSURL types:

1
2
3
4
5
6
+ (NSValueTransformer *)urlJSONTransformer{
    return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)isVipJSONTransformer{
    return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName];
}

Finally, for the conversion of the age field:

1
2
3
4
5
6
7
+ (NSValueTransformer *)ageJSONTransformer{
    return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *string, BOOL *success, NSError *__autoreleasing *error) {
        return @([string integerValue]);
    } reverseBlock:^id(NSNumber *number, BOOL *success, NSError *__autoreleasing *error) {
        return [number stringValue];
    }];
}

The reason returning NSNumber becomes NSUInteger inside the model is thanks to KVC. KVC can automatically box or unbox numeric or struct values into NSNumber or NSValue objects.

Handling Null Values

Let’s look at a piece of code first.

1
2
3
4
5
6
7
8
9
    NSDictionary *response = @{@"id" : @"1",
                          @"phone" : @"xxxxxx",
                          @"date" : @"2014-09-09",
                          @"goldNumber" : @2,
                          @"age" : @"18",
                          @"url" : @"http://bawn.github.io/",
                          @"isVip" : NSNull.null
                          };
    Member *member = [MTLJSONAdapter modelOfClass:[Member class] fromJSONDictionary:response error:nil];

This simulates the server returning a null value for the isVip field. The result is, of course, a crash. Mantle also provides a solution for this case: it converts the value to nil internally, and then we only need to implement - (void)setNilValueForKey:(NSString *)key;.

Member.m

1
2
3
4
5
6
7
8
- (void)setNilValueForKey:(NSString *)key{
    if ([key isEqualToString:@"isVip"]) {
        self.isVip = 0;
    }
    else{
        [super setNilValueForKey:key];
    }
}

This issue only applies to non-pointer types, such as float, bool, and double.

Pitfall

In general, when a field value is a URL, it is usually handled like this:

1
2
+ (NSValueTransformer *)linkJSONTransformer{
    return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];

But if the server returns a urlString like this: http://www.luisaviaroma.com/index.aspx?#ItemSrv.ashx|SeasonId=63I, Note: there is a | character in the string. With this kind of escape character, Mantle will fail during mapping, causing the entire model to become nil, which is difficult to debug.

So if the entire model unexpectedly becomes nil, check whether this situation exists, or avoid using this method and instead use:

1
2
3
4
5
+ (NSValueTransformer *)linkJSONTransformer{
    return [MTLValueTransformer transformerUsingForwardBlock:^id(id value, BOOL *success, NSError *__autoreleasing *error) {
        return [NSURL URLWithString:value];
    }];
}

Mantle also provides a class specifically for working with Core Data, MTLManagedObjectAdapter, which includes some very useful methods such as uniqueness checks and entity property conversion. In my next post, I will focus on using MagicalRecord together with Mantle.

Finally

Mantle of course has other features as well:

  • Archiving: the NSCoding protocol is already implemented
  • Comparison: - (BOOL)isEqual:(id)object;, with a default -hash implementation

Demo: MagicalRecord-Mantle

This post is licensed under CC BY 4.0 by the author.