[iOS-Vendor] Mantle

结合数据层返回的 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 类实现了[+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且实现了协议。在初始化方法中,保存了 Model 类[+ JSONKeyPathsByPropertyKey]方法返回的映射字典到属性JSONKeyPathsByPropertyKey。之后循环检查这个映射字典,看作为键的 property 名称是否在 Model 类的[+ propertyKeys]方法返回的 property 名集合中都存在,然后检查映射字典中表示 JSON 键的值是否符合类型要求。接着获取每一个 property 对应的 transformer,并以 property 名作为键保存到字典属性valueTransformersByPropertyKey中。获取 transformer 的过程是,先尝试 Model 类是否实现了[+JSONTransformer]方法,如果没有则继续看[+ 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类,它继承自MTLValueTransformerMTLValueTransformer实现[+ 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。

你可能感兴趣的:([iOS-Vendor] Mantle)