Mantle使用(摘自GitHub)

Mantle

Mantle可以容易的编写一个简单的CocoaCocoa touch应用程序的模型层。

典型的模型对象

通常我们用Objective-C写的模型层遇到了什么问题?

我们可以用Github API来举例。现在假设我们想用Objective-C展现一个Github Issue,应该怎么做?

typedef enum : NSUInteger {
    GHIssueStateOpen,
    GHIssueStateClosed
} GHIssueState;

@interface GHIssue : NSObject <NSCoding, NSCopying>

@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, copy, readonly) NSDate *updatedAt;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *retrievedAt;

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;

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

@end

@implementation GHIssue

+ (NSDateFormatter *)dateFormatter {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
    dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
    return dateFormatter;
}

- (id)initWithDictionary:(NSDictionary *)dictionary {
    self = [self init];
    if (self == nil) return nil;

    _URL = [NSURL URLWithString:dictionary[@"url"]];
    _HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]];
    _number = dictionary[@"number"];

    if ([dictionary[@"state"] isEqualToString:@"open"]) {
        _state = GHIssueStateOpen;
    } else if ([dictionary[@"state"] isEqualToString:@"closed"]) {
        _state = GHIssueStateClosed;
    }

    _title = [dictionary[@"title"] copy];
    _retrievedAt = [NSDate date];
    _body = [dictionary[@"body"] copy];
    _reporterLogin = [dictionary[@"user"][@"login"] copy];
    _assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]];

    _updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];

    return self;
}

- (id)initWithCoder:(NSCoder *)coder {
    self = [self init];
    if (self == nil) return nil;

    _URL = [coder decodeObjectForKey:@"URL"];
    _HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
    _number = [coder decodeObjectForKey:@"number"];
    _state = [coder decodeUnsignedIntegerForKey:@"state"];
    _title = [coder decodeObjectForKey:@"title"];
    _retrievedAt = [NSDate date];
    _body = [coder decodeObjectForKey:@"body"];
    _reporterLogin = [coder decodeObjectForKey:@"reporterLogin"];
    _assignee = [coder decodeObjectForKey:@"assignee"];
    _updatedAt = [coder decodeObjectForKey:@"updatedAt"];

    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
    if (self.URL != nil) [coder encodeObject:self.URL forKey:@"URL"];
    if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURL forKey:@"HTMLURL"];
    if (self.number != nil) [coder encodeObject:self.number forKey:@"number"];
    if (self.title != nil) [coder encodeObject:self.title forKey:@"title"];
    if (self.body != nil) [coder encodeObject:self.body forKey:@"body"];
    if (self.reporterLogin != nil) [coder encodeObject:self.reporterLogin forKey:@"reporterLogin"];
    if (self.assignee != nil) [coder encodeObject:self.assignee forKey:@"assignee"];
    if (self.updatedAt != nil) [coder encodeObject:self.updatedAt forKey:@"updatedAt"];

    [coder encodeUnsignedInteger:self.state forKey:@"state"];
}

- (id)copyWithZone:(NSZone *)zone {
    GHIssue *issue = [[self.class allocWithZone:zone] init];
    issue->_URL = self.URL;
    issue->_HTMLURL = self.HTMLURL;
    issue->_number = self.number;
    issue->_state = self.state;
    issue->_reporterLogin = self.reporterLogin;
    issue->_assignee = self.assignee;
    issue->_updatedAt = self.updatedAt;

    issue.title = self.title;
    issue->_retrievedAt = [NSDate date];
    issue.body = self.body;

    return issue;
}

- (NSUInteger)hash {
    return self.number.hash;
}

- (BOOL)isEqual:(GHIssue *)issue {
    if (![issue isKindOfClass:GHIssue.class]) return NO;

    return [self.number isEqual:issue.number] && [self.title isEqual:issue.title] && [self.body isEqual:issue.body];
}

你会看到,如此简单的事情却有很多弊端。甚至,还有一些其他问题,这个例子里面没有展示出来。

  • 无法使用服务器的新数据来更新这个 GHIssue
  • 无法反过来将 GHIssue 转换成 JSON
  • 对于GHIssueState,如果枚举改编了,现有的归档会崩溃
  • 如果 GHIssue 接口改变了,现有的归档会崩溃。

为什么不用 Core Data?

Core Data 能很好地解决这些问题. 如果你想在你的数据里面执行复杂的查询,处理很多关系,支持撤销恢复,Core Data非常适合。

然而,这样也带来了一些痛点:

  • 仍然有很多弊端Managed objects解决了上面看到的一些弊端,但是Core Data自生也有他的弊端。正确的配置Core Data和获取数据需要很多行代码。
  • 很难保持正确性。甚至有经验的人在使用Core Data时也会犯错,并且这些问题框架是无法解决的。

如果你想获取JSON对象,Core Data需要做很多工作,但是却只能得到很少的回报。

但是,如果你已经在你的APP里面使用了Core Data,Mantle将仍然会是你的API和你的managed model objects之间一个很方便的转换层。

MTLModel

如果使用MTLModel,我们可以这样,声明一个类继承自MTLModel

typedef enum : NSUInteger {
    GHIssueStateOpen,
    GHIssueStateClosed
} GHIssueState;

@interface GHIssue : MTLModel <MTLJSONSerializing>

@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *updatedAt;

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;

@property (nonatomic, copy, readonly) NSDate *retrievedAt;

@end

@implementation GHIssue

+ (NSDateFormatter *)dateFormatter {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
    dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
    return dateFormatter;
}

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{
        @"URL": @"url",
        @"HTMLURL": @"html_url",
        @"number": @"number",
        @"state": @"state",
        @"reporterLogin": @"user.login",
        @"assignee": @"assignee",
        @"updatedAt": @"updated_at"
    };
}

+ (NSValueTransformer *)URLJSONTransformer {
    return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}

+ (NSValueTransformer *)HTMLURLJSONTransformer {
    return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}

+ (NSValueTransformer *)stateJSONTransformer {
    return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
        @"open": @(GHIssueStateOpen),
        @"closed": @(GHIssueStateClosed)
    }];
}

+ (NSValueTransformer *)assigneeJSONTransformer {
    return [MTLJSONAdapter dictionaryTransformerWithModelClass:GHUser.class];
}

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

- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
    self = [super initWithDictionary:dictionaryValue error:error];
    if (self == nil) return nil;

    // Store a value that needs to be determined locally upon initialization.
    _retrievedAt = [NSDate date];

    return self;
}

@end

很明显,我们不需要再去实现<NSCoding><NSCopying>-isEqual:-hash。在你的子类里面生命属性,MTLModel可以提供这些方法的默认实现。

最初例子里面的问题,在这里都得到了很好的解决。

  • 无法使用服务器的新数据来更新这个 GHIssue
MTLModel提供了一个 - (void)mergeValueForKey:(NSString *)key fromModel:(id<MTLModel>)model{} ,可以与其他任何实现了MTLModel协议的模型对象集成。
    • 无法反过来将 GHIssue 转换成 JSON
    +[MTLJSONAdapter JSONDictionaryFromModel:error:] 可以把任何遵循 MTLJSONSerializing>``协议的对象转换成JSON字典, +[MTLJSONAdapter JSONArrayFromModels:error:] ` 类似,不过转换的是一个数组。
      • 如果 GHIssue 接口改变了,现有的归档会崩溃。
      MTLModel可以用归档很好的存储模型而不需要去实现令人厌烦的NSCoding协议。  -decodeValueForKey:withCoder:modelVersion: 方法在解码时会自动调用,如果重写,可以方便的进行自定义。

      MTLJSONSerializing

      为了你的模型对象或JSON序列化,你需要实现< MTLJSONSerializing > 在MTLModel的子类。这允许您使用MTLJSONAdapter转换模型从JSON对象并返回:

      NSError *error = nil;
      XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];

      NSError *error = nil;
      NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:user error:&error];

      +JSONKeyPathsByPropertyKey

      这个方法返回的字典指定你的模型对象的属性映射到JSON,例如:
      @interface XYUser : MTLModel
      
      @property (readonly, nonatomic, copy) NSString *name;
      @property (readonly, nonatomic, strong) NSDate *createdAt;
      
      @property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
      @property (readonly, nonatomic, strong) XYHelper *helper;
      
      @end
      
      @implementation XYUser
      
      + (NSDictionary *)JSONKeyPathsByPropertyKey {
          return @{
              @"name": @"name",
              @"createdAt": @"created_at"
          };
      }
      
      - (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
          self = [super initWithDictionary:dictionaryValue error:error];
          if (self == nil) return nil;
      
          _helper = [XYHelper helperWithName:self.name createdAt:self.createdAt];
      
          return self;
      }
      
      @end
      在这个例子中,XYUser类声明四个属性,Mantle以不同的方式处理:
      • name 相同名称的名称映射到一个JSON反序列化。
      • createdAt 被转换其等效.
      • meUser 没有被映射到一个JSON.
      • helper 在JSON反序列化后完全被初始化

      用 -[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:] 如果你的 model类遵循 MTLJSONSerializing 合并他们的映射

      如果你想一个模型类的所有属性映射到自己, 你可以用 +[NSDictionary mtl_identityPropertyMapWithModel:] 

      当反序列化JSON 用 +[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:], JSON键没有对应一个属性名或没有显式的映射将被忽略:

      NSDictionary *JSONDictionary = @{
          @"name": @"john",
          @"created_at": @"2013/07/02 16:40:00 +0000",
          @"plan": @"lite"
      };
      
      XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
      在这里,plan将被忽略,因为它既不匹配属性名 + JSONKeyPathsByPropertyKey XYUser也不是否则映射。

      +JSONTransformerForKey:

      实现这个可选方法转换属性从不同类型当反序列化JSON。

      + (NSValueTransformer *)JSONTransformerForKey:(NSString *)key {
          if ([key isEqualToString:@"createdAt"]) {
              return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName];
          }
      
          return nil;
      }

      key 是你的模型对象,而不是原始JSON的key. 牢记这一点,如果你使用 +JSONKeyPathsByPropertyKey.

      为了增加方便, 如果你实现了  +<key>JSONTransformer MTLJSONAdapter  将使用该方法的返回结果 例如, 日期,通常表示为JSON字符串可以转化为nsdate一样:
      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];
          }];
      }
      如果 transformer是可逆的,它也将使用在序列化的对象转换为JSON。

      +classForParsingJSONDictionary:

      如果你是实现一个类集群,实现这个可选的方法来确定你的基类的子类时应使用反序列化一个对象从JSON。
      @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

      MTLJSONAdapter会选择基于JSON类字典传递:

      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];

      持久性

      Mantle不自动为您保存对象。然而,MTLModel并符合< NSCoding >,所以模型对象可以使用NSKeyedArchiver归档到磁盘。如果你需要一些更强大的,或者想要避免在内存中保持你的整个模型, Core Data可能是一个更好的选择。


      formhttps://github.com/Mantle/Mantle

      • 0
        点赞
      • 0
        收藏
        觉得还不错? 一键收藏
      • 0
        评论
      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包
      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

      余额充值