字典转模型框架 Mantle的使用:国外程序员最常用的iOS模型

Mantle简介

Mantle 是iOS和Mac平台下基于Objective-C编写的一个简单高效的模型层框架。

Mantle能做什么

Mantle可以轻松把JSON数据、字典(Dictionary)和模型(即Objective对象)之间的相互转换,支持自定义映射,并且内置实现了NSCoding和NSCoping,大大简化归档操作。

为什么要使用Mantle

传统的模型层方案遇到的问题

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

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

目前我们可以想到

  1. 直接解析JSON数据字典,然后展现给UI
  2. 将JSON数据转换为模型,在赋值给UI

关于1,弊端有很多,可以参考我的这篇文章:  在iOS开发中使用字典转模型 ,现在假设我们选择了2,我们大致会定义下面的  GHIssue 模型:

GHIssue.h

  #import <Foundation/Foundation.h>

  typedef enum : NSUInteger {
      GHIssueStateOpen,
      GHIssueStateClosed
  } GHIssueState;

  @class GHUser;
  @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;

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

  @end

GHIssue.m

  #import "GHIssue.h"
  #import "GHUser.h"

  @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;
  }

  - (instancetype)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;
  }

  - (instancetype)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 decodeIntegerForKey:@"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 encodeInteger:self.state forKey:@"state"];
  }

  - (instancetype)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];
  }

GHUser.h

  @interface GHUser : NSObject <NSCoding, NSCopying>

  @property (nonatomic, copy) NSString *login;
  @property (nonatomic, assign) NSUInteger id;
  @property (nonatomic, copy) NSString *avatarUrl;
  @property (nonatomic, copy) NSString *gravatarId;
  @property (nonatomic, copy) NSString *url;
  @property (nonatomic, copy) NSString *htmlUrl;
  @property (nonatomic, copy) NSString *followersUrl;
  @property (nonatomic, copy) NSString *followingUrl;
  @property (nonatomic, copy) NSString *gistsUrl;
  @property (nonatomic, copy) NSString *starredUrl;
  @property (nonatomic, copy) NSString *subscriptionsUrl;
  @property (nonatomic, copy) NSString *organizationsUrl;
  @property (nonatomic, copy) NSString *reposUrl;
  @property (nonatomic, copy) NSString *eventsUrl;
  @property (nonatomic, copy) NSString *receivedEventsUrl;
  @property (nonatomic, copy) NSString *type;
  @property (nonatomic, assign) BOOL siteAdmin;

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

  @end

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

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

使用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可以提供这些方法的默认实现。

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

  • MTLModel提供了一个  - (void)mergeValueForKey:(NSString *)key fromModel:(id<MTLModel>)model{} ,可以与其他任何实现了MTLModel协议的模型对象集成。
  • +[MTLJSONAdapter JSONDictionaryFromModel:error:] 可以把任何遵循  MTLJSONSerializing>``协议的对象转换成JSON字典, +[MTLJSONAdapter JSONArrayFromModels:error:]```类似,不过转换的是一个数组。

MTLJSONAdapter 中的  fromJSONDictionary 和  JSONDictionaryFromModel 可以实现模型和JSON的相互转化。

JSONKeyPathsByPropertyKey 可以实现模型和JSON的自定义映射。

JSONTransformerForKey 可以对JSON和模型不同类型进行映射。

classForParsingJSONDictionary 如果你使用了类簇(关于类簇,请参考: 类簇在iOS开发中的应用 ),classForParsingJSONDictionary可以让你选择使用哪一个类进行JSON反序列化。

  • MTLModel可以用归档很好的存储模型而不需要去实现令人厌烦的NSCoding协议。  -decodeValueForKey:withCoder:modelVersion: 方法在解码时会自动调用,如果重写,可以方便的进行自定义。

持久化

Mantle配合归档

MTLModel默认实现了  NSCoding 协议,可以利用  NSKeyedArchiver 方便的对对象进行归档和解档。

Mantle配合Core Data

除了SQLite、FMDB之外,如果你想在你的数据里面执行复杂的查询,处理很多关系,支持撤销恢复,Core Data非常适合。

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

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

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

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

Mantle配合MagicRecord(一个Core Data框架)

参考  MagicalRecord配合Mantle

Mantle为我们带来的好处

  • 实现了NSCopying protocol,子类可以直接copy是多么爽的事情
  • 实现了NSCoding protocol,跟NSUserDefaults说拜拜
  • 提供了-isEqual:和-hash的默认实现,model作NSDictionary的key方便了许多
  • 支持自定义映射,这在接口改变的情况下很有用
  • 简单且把一件事情做好,不掺杂网络相关的操作

合理选择

虽然上面说了一系列的好处,但如果你的App的代码规模只有几万行,或者API只有十几个,或者没有遇到上面这些问题, 建议还是不要引入了,杀鸡用指甲刀就够了。但是,Mantle的实现和思路是值得每位iOS工程师学习和借鉴的。

代码

https://github.com/terwer/MantleDemo

参考

https://github.com/mantle/mantle

http://segmentfault.com/a/1190000002431365

时间: 2024-10-27 03:16:37

字典转模型框架 Mantle的使用:国外程序员最常用的iOS模型的相关文章

推荐!国外程序员整理的机器学习资源大全

推荐!国外程序员整理的机器学习资源大全 本文汇编了一些机器学习领域的框架.库以及软件(按编程语言排序). 伯乐在线已在 GitHub 上发起「机器学习资源大全中文版」的整理.欢迎扩散.欢迎加入. https://github.com/jobbole/awesome-machine-learning-cn C++ 计算机视觉 CCV —基于C语言/提供缓存/核心的机器视觉库,新颖的机器视觉库 OpenCV—它提供C++, C, Python, Java 以及 MATLAB接口,并支持Windows

推荐!国外程序员整理的机器学习资源大全(转)

本文由 伯乐在线 - toolate 翻译自 awesome-machine-learning.欢迎加入技术翻译小组.转载请参见文章末尾处的要求. 本文汇编了一些机器学习领域的框架.库以及软件(按编程语言排序). C++ 计算机视觉 CCV —基于C语言/提供缓存/核心的机器视觉库,新颖的机器视觉库 OpenCV—它提供C++, C, Python, Java 以及 MATLAB接口,并支持Windows, Linux, Android and Mac OS操作系统. 通用机器学习 MLPack

为什么国外程序员爱用 Mac?

from http://www.vpsee.com/2009/06/why-programmers-love-mac/ Mac 在国外很受欢迎,尤其是在 设计/web开发/IT 人员圈子里.普通用户喜欢 Mac 可以理解,毕竟 Mac 设计美观,简单好用,没有病毒.那么为什么专业人士也对 Mac 情有独钟呢?从个人使用经验来看我想有下面几个原因: 1.Mac OS X 是基于 Unix 的.这一点太重要了,尤其是对开发人员,至少对于我来说很重要,这意味着Unix 下一堆好用的工具都可以随手捡到.

【转】国外程序员整理的Java资源大全

Java几乎是许多程序员们的入门语言,并且也是世界上非常流行的编程语言.国外程序员Andreas Kull在其Github上整理了非常优秀的Java开发资源,推荐给大家.译文由ImportNew- 唐尤华翻译完成. 以下为具体资源列表. 构建 这里搜集了用来构建应用程序的工具. Apache Maven:Maven使用声明进行构建并进行依赖管理,偏向于使用约定而不是配置进行构建.Maven优于Apache Ant.后者采用了一种过程化的方式进行配置,所以维护起来相当困难. Gradle:Grad

国外程序员整理的Java资源大全分享

Java 几乎是许多程序员们的入门语言,并且也是世界上非常流行的编程语言.国外程序员 Andreas Kull 在其 Github 上整理了非常优秀的 Java 开发资源,推荐给大家. 译文由 ImportNew- 唐尤华翻译完成. 以下为具体资源列表. 构建 这里搜集了用来构建应用程序的工具. Apache Maven:Maven 使用声明进行构建并进行依赖管理,偏向于使用约定而不是配置进行构建.Maven 优于 Apache Ant.后者采用了一种过程化的方式进行配置,所以维护起来相当困难.

转:为什么国外程序员爱用 Mac?

Mac 在国外很受欢迎,尤其是在 设计/web开发/IT 人员圈子里.普通用户喜欢 Mac 可以理解,毕竟 Mac 设计美观,简单好用,没有病毒.那么为什么专业人士也对 Mac 情有独钟呢?从个人使用经验来看我想有下面几个原因: 1.Mac OS X 是基于 Unix 的.这一点太重要了,尤其是对开发人员,至少对于我来说很重要,这意味着Unix 下一堆好用的工具都可以随手捡到.如果你是个 windows 开发人员,我想你会在 windows 上装一套cygwin 环境吧?你不用 flex/yac

为什么国外程序员爱用苹果Mac电脑?(转)

Mac 在国外很受欢迎,尤其是在 设计/web开发/IT 人员圈子里.普通用户喜欢 Mac 可以理解,毕竟 Mac 设计美观,简单好用,没有病毒.那么为什么专业人士也对 Mac 情有独钟呢?从个人使用经验来看我想有下面几个原因: 1.Mac OS X 是基于 Unix 的.这一点太重要了,尤其是对开发人员,至少对于我来说很重要,这意味着Unix 下一堆好用的工具都可以随手捡到.如果你是个 windows 开发人员,我想你会在 windows 上装一套cygwin 环境吧?你不用 flex/yac

国外程序员推荐:每个程序员都应该读的非编程书

1. <银河系漫游指南>by Douglas Adams 2. <人性的弱点> by Dale Carnegie 3. <别逗了,费曼先生> 4. <一九八四> by George Orwell 5. <哥德尔.艾舍尔.巴赫:集异璧之大成> by Douglas Hofstadter 6. <设计心理学> by Donald A. Norman 7. <搞定:无压工作的艺术>by David Allen 8. <人月

为什么国外程序员爱用苹果 Mac 电脑?

Mac 在国外很受欢迎,尤其是在 设计/web开发/IT 人员圈子里.普通用户喜欢 Mac 可以理解,毕竟 Mac 设计美观,简单好用,没有病毒.那么为什么专业人士也对 Mac 情有独钟呢?从个人使用经验来看我想有下面几个原因: 1.Mac OS X 是基于 Unix 的.这一点太重要了,尤其是对开发人员,至少对于我来说很重要,这意味着Unix 下一堆好用的工具都可以随手捡到.如果你是个 windows 开发人员,我想你会在 windows 上装一套cygwin 环境吧?你不用 flex/yac