结合数据层返回的 JSON 数据,Mantle 在基于 MVC 模式的应用的 Model 层中发挥了重要作用,包括 Model 类与 JSON 字典之间的相互转换,Model 类的序列化与反序列化等。
使用
如果不使用 Mantle,当把 Model 层的工作放到 Model 类中时,Model 类的实现基本是这个样子的:
@interface VDPersonModel : NSObject
@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
@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
类,根据子类声明的属性,提供了
,
, -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)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字典之间进行相互转换,相当于是一个适配器。 -
协议:需要与JSON字典进行相互转换的MTLModel的子类都需要实现该协议,以方便MTLJSONApadter对象进行转换。
如果MTLModel
的子类希望可以使用MTLJSONAdapter
类做 Model 类与 JSON 字典间的转换,则必须实现
协议
@protocol MTLJSONSerializing
@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为 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
且实现了
协议。在初始化方法中,保存了 Model 类[+ JSONKeyPathsByPropertyKey]
方法返回的映射字典到属性JSONKeyPathsByPropertyKey
。之后循环检查这个映射字典,看作为键的 property 名称是否在 Model 类的[+ propertyKeys]
方法返回的 property 名集合中都存在,然后检查映射字典中表示 JSON 键的值是否符合类型要求。接着获取每一个 property 对应的 transformer,并以 property 名作为键保存到字典属性valueTransformersByPropertyKey
中。获取 transformer 的过程是,先尝试 Model 类是否实现了[+
方法,如果没有则继续看[+ JSONTransformerForKey:]
方法是否有返回,若依然没有则会根据 property 的类型自动创建一个 transformer。比如,如果 property 的类型也是一个继承自MTLModel
且实现了
协议的 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)model error:(NSError **)error;
// Model 的数组转为字典的数组
+ (NSArray *)JSONArrayFromModels:(NSArray *)models error:(NSError **)error;
Adapter 所提供的四个便捷方法,其实内部就是根据 Model class 创建了一个MTLJSONAdapter
实例,并调用了[- modelFromJSONDictionary:error:]
或[- JSONDictionaryFromModel:error:]
方法。
MTLModel
MTLModel
是所有 Model 类要继承的抽象基类,它提供了一些方法的默认实现:
-
,通过运行时实现了所有 property 的归档。 -
,复制一个新的对象,并设置 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 *)valueTransformerNames;
MTLValueTransformer
Mantle 实现了MTLValueTransformer
作为NSValueTransformer
的派生类
// 定义提供给使用者的实现自定义转换逻辑的 block,value 为待转换的值,使用者可以通过 success 和 error 指明,转换是否成功及失败的原因。
typedef id (^MTLValueTransformerBlock)(id value, BOOL *success, NSError **error);
@interface MTLValueTransformer : NSValueTransformer
// 根据传入的转换 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
类,它继承自MTLValueTransformer
。MTLValueTransformer
实现[+ allowsReverseTransformation]
方法返回 NO,而MTLReversibleValueTransformer
重写该方法返回 YES。这样除[+ transformerUsingForwardBlock:]
方法返回MTLValueTransformer
实例外,另外两个构造器方法真正返回的都是MTLReversibleValueTransformer
实例。
MTLValueTransformer
整个功能的实现比较简单,定义指定初始化函数[- initWithForwardBlock: reverseBlock:]
,在其中通过成员变量保存了传入的转换 block。之后在转换方法的实现中,执行对应 block 即可。
另外,Mantle 还定义了一个协议
,其中包含两个方法:
@protocol MTLTransformerErrorHandling
@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。