[iOS-Vendor] Mantle

结合数据层返回的 JSON 数据,Mantle 在基于 MVC 模式的应用的 Model 层中发挥了重要作用,包括 Model 类与 JSON 字典之间的相互转换,Model 类的序列化与反序列化等。

使用

如果不使用 Mantle,当把 Model 层的工作放到 Model 类中时,Model 类的实现基本是这个样子的:

@interface VDPersonModel : NSObject <NSCoding, NSCopying>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSDate *birthday;
@property (nonatomic) NSUInteger age;

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

@end

static NSString * const kPropertyKeyName = @"name";
static NSString * const kPropertyKeyAge = @"age";
static NSString * const kPropertyKeyBirthday = @"birthday";

@implementation VDPersonModel

+ (NSDateFormatter *)dateFormatter {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss";
    
    return dateFormatter;
}

- (id)initWithDictionary:(NSDictionary *)dictionary {
    if (self = [super init]) {
        self.name = dictionary[kPropertyKeyName];
        self.age = [dictionary[kPropertyKeyAge] integerValue];
        self.birthday = [self.class.dateFormatter dateFromString:dictionary[kPropertyKeyBirthday]];
    }
    
    return self;
}

- (NSDictionary *)dictionaryFromModel {
    NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
    if (self.name) dictionary[kPropertyKeyName] = self.name;
    if (self.birthday) dictionary[kPropertyKeyBirthday] = [self.class.dateFormatter stringFromDate.birthday];
    dictionary[kPropertyKeyAge] = @(self.age);
    
    return [dictionary copy];
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        self.name = [aDecoder decodeObjectForKey:kPropertyKeyName];
        self.birthday = [aDecoder decodeObjectForKey:kPropertyKeyBirthday];
        self.age = [aDecoder decodeIntegerForKey:kPropertyKeyAge];
    }
    
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    if (self.name) [aCoder encodeObject:self.name forKey:kPropertyKeyName];
    if (self.birthday) [aCoder encodeObject:self.birthday forKey:kPropertyKeyBirthday];
    [aCoder encodeInteger:self.age forKey:kPropertyKeyAge];
}

- (id)copyWithZone:(NSZone *)zone {
    VDPersonModel *person = [[self.class allocWithZone:zone] init];
    person.name = self.name;
    person.birthday = self.birthday;
    person.age = self.age;
    
    return person;
}

- (BOOL)isEqual:(VDPersonModel *)person {
    if (![person isKindOfClass:self.class]) return NO;
    
    return [self.name isEqual:person.name] && [self.birthday isEqual:person.birthday] && self.age == person.age;
}

@end

基本上应用中的所有 Model 类都要以这个模板来实现,使用 Mantle 框架后,Model 层的实现变成了这个样子:

@interface VDPersonModel : MTLModel <MTLJSONSerializing>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSDate *birthday;
@property (nonatomic) NSUInteger age;

@end

static NSString * const kPropertyKeyName = @"name";
static NSString * const kPropertyKeyAge = @"age";
static NSString * const kPropertyKeyBirthday = @"birthday";

@implementation VDPersonModel

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{
             @"name"        : kPropertyKeyName,
             @"birthday"    : kPropertyKeyBirthday,
             @"age"         : kPropertyKeyAge
             };
}

+ (NSValueTransformer *)birthdayJSONTransformer {
    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];
    }];
}

+ (NSDateFormatter *)dateFormatter {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss";
    
    return dateFormatter;
}

@end

Model 类除了提供属性与 JSON 字典键值的映射以及不同类型的转换外,其他工作 Mantle 都已提供实现,这样就减少了大量模板代码的编写。Model 类继承的MTLModel类,根据子类声明的属性,提供了<NSCoding>, <NSCopying>, -isEqual:的相关实现。对于 Model 类与 JSON 字典的转换,工具类MTLJSONAdapter提供了相关方法:

// 字典转为 Model
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error;
// 字典的数组转为 Model 的数组
+ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error;
// Model 转为字典
+ (NSDictionary *)JSONDictionaryFromModel:(id<MTLJSONSerializing>)model error:(NSError **)error;
// Model 的数组转为字典的数组
+ (NSArray *)JSONArrayFromModels:(NSArray *)models error:(NSError **)error;

转换实例:

NSDictionary *dictionary = @{
                                 @"name" : @"Mark",
                                 @"birthday" : @"2016-11-23 23:59:59",
                                 @"age" : @99
                                 };
NSError *error;
VDPersonModel *person = [MTLJSONAdapter modelOfClass:VDPersonModel.class fromJSONDictionary:dictionary error:&error];
NSDictionary *personDictionary = [MTLJSONAdapter JSONDictionaryFromModel:person error:&error];

分析

Mantle 中的核心内容可以分为以下几部分:

  • MTLModel类:通常是作为 Model 的基类,该类提供了一些默认的行为来处理对象的初始化和归档操作,同时可以获取到对象所有属性的键值集合。
  • MTLJSONAdapter类:用于在MTLModel对象和JSON字典之间进行相互转换,相当于是一个适配器。
  • <MTLJSONSerializing>协议:需要与JSON字典进行相互转换的MTLModel的子类都需要实现该协议,以方便MTLJSONApadter对象进行转换。
<MTLJSONSerializing>

如果MTLModel的子类希望可以使用MTLJSONAdapter类做 Model 类与 JSON 字典间的转换,则必须实现<MTLJSONSerializing>协议

@protocol MTLJSONSerializing <MTLModel>
@required
+ (NSDictionary *)JSONKeyPathsByPropertyKey;
@optional
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary;
@end

通过方法[+ JSONKeyPathsByPropertyKey]返回的字典,定义了 Model 类的属性名与 JSON 字典键的映射,在这个返回的字典中,Model 类的属性名作为键,JSON 字典的键作为值。如果父类也实现了该协议,子类在重写该方法时,需要将父类的结果也包括进来的。表示JSON 字典键的值,可以是单个的键,也可以是以.连接的键的路径,或者是一个以上两者组成的数组。数组对应的 property 得到的值是数组中各键及对应的 JSON 值组成的字典。如:

// 属性 name 对应 JSONDictionary[@"POI"][@"name"]
// 属性 starred 对应 JSONDictionary[@"starred"]
// 属性 point 对应 @{
//    @"latitude": JSONDictionary[@"latitude"],
//    @"longitude": JSONDictionary[@"longitude"]
// }
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{
        @"name": @"POI.name",
        @"point": @[ @"latitude", @"longitude" ],
        @"starred": @"starred"
    };
}

实现可选方法[+ JSONTransformerForKey:]返回一个NSValueTransformer对象,可以实现 Model 类与 JSON 字典间互相转换时,改变原始数据类型。例如,代表 API 响应的 JSON 字典中日期类的值一般为字符串,而在 Model 类中对应的 property 一般为 NSDate类型,此时就可以在该方法中实现转换的逻辑。该方法传入的参数 key,为 Model 类的属性名,而不是 JSON 字典的键值。如果 Model 类实现了[+<key>JSONTransformer]方法(key为 Model 类的属性名),则该方法会替代[+ JSONTransformerForKey:]方法被调用。

如果有一个类簇,基类或抽象类可以实现方法[+ classForParsingJSONDictionary:],根据传入的需要转换的 JSON 字典,可以指定具体的对应 Model 子类。

@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

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];
MTLJSONAdapter
初始化方法

MTLJSONAdapter禁用了[- init]方法,重新实现了新的指定初始化函数[- initWithModelClass:],该方法需要传入一个 Model 类的 Class 类型值,注意,这个 Model 类必须继承自MTLModel且实现了<MTLJSONSerializing>协议。在初始化方法中,保存了 Model 类[+ JSONKeyPathsByPropertyKey]方法返回的映射字典到属性JSONKeyPathsByPropertyKey。之后循环检查这个映射字典,看作为键的 property 名称是否在 Model 类的[+ propertyKeys]方法返回的 property 名集合中都存在,然后检查映射字典中表示 JSON 键的值是否符合类型要求。接着获取每一个 property 对应的 transformer,并以 property 名作为键保存到字典属性valueTransformersByPropertyKey中。获取 transformer 的过程是,先尝试 Model 类是否实现了[+<key>JSONTransformer]方法,如果没有则继续看[+ JSONTransformerForKey:]方法是否有返回,若依然没有则会根据 property 的类型自动创建一个 transformer。比如,如果 property 的类型也是一个继承自MTLModel且实现了<MTLJSONSerializing>协议的 Model 类,则会创建一个使用 Adapter 转换 JSON 字典的 transformer。

JSON 转 Model

JSON 转 Model 时,调用 Adapter 实例的[- modelFromJSONDictionary: error:]方法,该方法首先检查关联的 Model 类是否实现了方法[+ classForParsingJSONDictionary:],若这个方法返回了关联 Model 类的子类,则为该子类创建新的 Adapter 并返回新 Adapter 的该方法的返回值。处理子类的情况后,则循环 Model 类[+ propertyKeys]返回的数组。在循环中首先根据属性名从 Adapter 的JSONKeyPathsByPropertyKey属性中取出对应的 JSON 键,这个 JSON 键可以是单个的键,也可以是以.连接的键的路径,或者是一个以上两者组成的数组,因为键值可以是一个路径,所以 Mantle 实现了字典的分类方法[- mtl_valueForJSONKeyPath:success:error:]用来取值,若取值失败则最终导致转换失败,而数组键对应的值是数组中各键及对应的 JSON 值组成的字典。在成功取得 property 对应的值后,该方法会从valueTransformersByPropertyKey取出 property 对应的 transformer 来进行值的转换,若值的转换失败,则该方法返回 nil,即转换 Model 失败。最后将 property 与其获取到的转换后的值放入一个新的字典中。在循环结束后,调用 Model 类的方法[+ modelWithDictionary:error:],传入之前获得的 property 与对应值的字典,生成一个 Model 实例。最后还要调用一下 Model 实例的校验方法[- validate:],如果这两步都没有发生错误的话,则转换 Model 实例成功。

Model 转 JSON

Model 转 JSON 时,调用 Adapter 实例的[- JSONDictionaryFromModel:error:]方法,该方法首先判断如果传入的 Model 实例是关联 Model 类的子类,则根据子类创建新的 Adapter 并返回新 Adapter 的该方法的返回值。处理子类的情况后,通过 Model 类的dictionaryValue属性获取 Model 类 property 名与值的映射字典,接着循环这个字典并做转换操作。在循环中,首先从JSONKeyPathsByPropertyKey中取出 property 对应 JSON 键,接着从valueTransformersByPropertyKey中取出 property 对应的 transformer 对 property 的值做转换。因为 JSON 键可能是路径,所以可能要嵌套的创建字典,若 JSON 键为数组,则要循环的执行这一操作。最后将转换后的值存入字典对应层级的对应键中,如果整个过程没有发生错误,则创建成功。

便捷方法
// 字典转为 Model
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error;
// 字典的数组转为 Model 的数组
+ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error;
// Model 转为字典
+ (NSDictionary *)JSONDictionaryFromModel:(id<MTLJSONSerializing>)model error:(NSError **)error;
// Model 的数组转为字典的数组
+ (NSArray *)JSONArrayFromModels:(NSArray *)models error:(NSError **)error;

Adapter 所提供的四个便捷方法,其实内部就是根据 Model class 创建了一个MTLJSONAdapter实例,并调用了[- modelFromJSONDictionary:error:][- JSONDictionaryFromModel:error:]方法。

MTLModel

MTLModel是所有 Model 类要继承的抽象基类,它提供了一些方法的默认实现:

  • <NSCoding>,通过运行时实现了所有 property 的归档。
  • <NSCopying>,复制一个新的对象,并设置 property 的值。
  • [- isEqual:],默认比较所有 property 的值。
  • [- description],返回描述所有 property 的字符串。

除了 Model 类所应实现的方法外,MTLModel还实现了一些辅助MTLJSONAdapter使用的方法,如:

  • [+ propertyKeys],该方法返回MTLModel类的子类中定义的所有 property 名组成的集合。在该方法中,首先调用了[+ enumeratePropertiesUsingBlock:]来递归遍历了 Model 类及父类直到MTLModel类为止的所有 property,在 block 参数中,property 以 objc_property_t类型返回。然后通过方法[+ storageBehaviorForPropertyWithKey:]过滤掉不符合规则的属性,将符合规则的属性名加到集合中。最后通过关联对象(Associated Object)的方式缓存处理过的属性名集合,这种缓存策略可以优化因为使用 runtime 造成的性能损耗。
  • [- initWithDictionary:error:],根据 property 名及对应值的字典初始化 Model 实例。对应的便捷构造方法是[+ modelWithDictionary:error:]。该方法用字典中的内容通过 KVC 的方式设置对应的 property 值。
  • [- validate:],默认实现是对所有的 property 执行 KVC 方法[- validateValue:forKey:error:]
ValueTransformer

很多时候,Model 类 property 的类型与对应 JSON 字典中的值类型并不相同,此时就需要使用者提供自定义的转换逻辑。例如,代表 API 响应的 JSON 字典中日期类型的值一般为字符串,而在 Model 类中对应的 property 一般为NSDate类型。Mantle 利用了 Foundation 中的 NSValueTransformer 来提供这种转换接口。

NSValueTransformer

NSValueTransformer是用于数据转换的抽象类,子类需要实现以下方法:

// 转换后类型
+ (Class)transformedValueClass;
// 指明是否可以双向转换
+ (BOOL)allowsReverseTransformation;
// 正向转换
- (id)transformedValue:(id)value;
// 逆向转换
- (id)reverseTransformedValue:(id)value;

NSValueTransformer提供了不需要每次都实例化具体的子类的机制,而是可以通过NSValueTransformer的类方法注册和调用实例:

// 为一个 transformer 实例注册一个对应的标识符
+ (void)setValueTransformer:(NSValueTransformer *)transformer  forName:(NSValueTransformerName)name;
// 根据标识符获取 transformer 实例
+ (NSValueTransformer *)valueTransformerForName:(NSValueTransformerName)name;
// 返回所有注册的 transformer 标识符
+ (NSArray<NSValueTransformerName> *)valueTransformerNames;
MTLValueTransformer

Mantle 实现了MTLValueTransformer作为NSValueTransformer的派生类

// 定义提供给使用者的实现自定义转换逻辑的 block,value 为待转换的值,使用者可以通过 success 和 error 指明,转换是否成功及失败的原因。
typedef id (^MTLValueTransformerBlock)(id value, BOOL *success, NSError **error);

@interface MTLValueTransformer : NSValueTransformer <MTLTransformerErrorHandling>
// 根据传入的转换 block 实例化一个正向 transformer
+ (instancetype)transformerUsingForwardBlock:(MTLValueTransformerBlock)transformation;
// 实例化一个 transformer,正向反向都使用传入的转换 block
+ (instancetype)transformerUsingReversibleBlock:(MTLValueTransformerBlock)transformation;
// 根据分别传入的不同的正向反向转换 block 实例化一个 transformer
+ (instancetype)transformerUsingForwardBlock:(MTLValueTransformerBlock)forwardTransformation reverseBlock:(MTLValueTransformerBlock)reverseTransformation;
@end

因为指明 transformer 是否支持逆向转换的方法[+ allowsReverseTransformation]是类级别的,所以在实现文件内部还定义了一个MTLReversibleValueTransformer类,它继承自MTLValueTransformerMTLValueTransformer实现[+ allowsReverseTransformation]方法返回 NO,而MTLReversibleValueTransformer重写该方法返回 YES。这样除[+ transformerUsingForwardBlock:]方法返回MTLValueTransformer实例外,另外两个构造器方法真正返回的都是MTLReversibleValueTransformer实例。

MTLValueTransformer整个功能的实现比较简单,定义指定初始化函数[- initWithForwardBlock: reverseBlock:],在其中通过成员变量保存了传入的转换 block。之后在转换方法的实现中,执行对应 block 即可。

另外,Mantle 还定义了一个协议<MTLTransformerErrorHandling>,其中包含两个方法:

@protocol MTLTransformerErrorHandling <NSObject>
@required
- (id)transformedValue:(id)value success:(BOOL *)success error:(NSError **)error;
@optional
- (id)reverseTransformedValue:(id)value success:(BOOL *)success error:(NSError **)error;
@end

MTLValueTransformer在实现这个协议后,当使用 transformer 实例转换一个值时,同时还可以获取到转换 block 中指定的 success 和 error。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容