文档章节

Mantle--国外程序员最常用的iOS模型&字典转换框架

ios122
 ios122
发布于 2015/10/20 16:28
字数 1780
阅读 110
收藏 6
点赞 0
评论 0

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中的fromJSONDictionaryJSONDictionaryFromModel可以实现模型和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

http://yyny.me/ios/Mantle%E3%80%81JSONModel%E3%80%81MJExtension%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/

PS: 本文由我们iOS122的小伙伴@TerwerGreen整理编辑,欢迎大家到他的个人博客terwer共同论道!

© 著作权归作者所有

共有 人打赏支持
ios122
粉丝 72
博文 76
码字总数 132179
作品 3
东城
程序员
iOS开发·runtime+KVC实现多层字典模型转换(多层数据:模型嵌套模型,模型嵌套数组,数组嵌套模型)

本文实验Demo传送门:DictToModelDemo 前言:将后台JSON数据中的字典转成本地的模型,我们一般选用部分优秀的第三方框架,如SBJSON、JSONKit、MJExtension、YYModel等。但是,一些简单的数据...

陈满iOS
05/08
0
0
【逐渐提高】ios 代码库 集合类

系统基础库 Category/Util sstoolkit 一套Category类型的库,附带很多自定义控件 功能不错~ BlocksKit 将Block风格带入UIKit和Founcation cocoa-helpers 一些Cocoa的扩展 2年前的工程 Coconu...

创意总监
2014/08/30
0
1
推荐几个牛逼的iOS开发常用框架及Xcode插件

管理工具 1. CocoaPods 搞iOS开发的应该都知道的,管理框架,并解决依赖问题。不可不用。下面用到的框架基本都可以用这个搜索和进行安装。怎么用搜搜教程就好了。和后文提到的配套使用,操作...

Harry_1234
2017/10/23
0
0
面试官自述:面向高级开发人员的iOS面试问题

当您准备进行技术性iOS面试时,了解您可能会询问哪些主题以及经验丰富的iOS开发人员期望什么是非常重要的。 这是许多硅谷公司用来衡量iOS候选人资历水平的一系列问题。 这些问题涉及iOS开发的...

菇哒微课
04/26
0
0
一样的iOS开发程序员为什么有人4k有人40k?

前言 移动开发真正火起来其实就是最近这几年,iOS 开发技术因为发展也就才这么几年,所以值得做的事情还有很多,这就造成了每年苹果的 WWDC 都会推出一堆新的特性和 API。整体上来说,这对业...

原来是泽镜啊
05/16
0
0
再看知名应用背后的第三方开源项目

知名应用程序的设计和技术一直都是开发者需要学习的,同样这些应用所使用的开源框架也是不可忽视的一部分。此前《iOS第三方开源库的吐槽和备忘》中作者ibireme列举了国内多款知名应用所使用的...

hejunbinlan
2015/08/18
0
0
iOS:JSON转OC属性小工具

iOS:JSON转OC属性小工具 在iOS开发中,只要有网络模块,就需要数据模型的编写。在进行数据模型的解析和映射时,JSONModel是一个非常常用且优秀的第三方框架,之前有有过博客对其分析,地址如...

珲少
06/09
0
0
谷歌Flutter跨平台应用开发SDK迎来首个发行预览版本

谷歌Flutter跨平台应用开发SDK迎来首个发行预览版本 2018-06-22 12:26编辑: 枣泥布丁分类:程序人生来源:程序师 跨平台Google Flutter预览版本 招聘信息: 图像处理及模式识别工程师 C/C+...

枣泥布丁
06/22
0
0
机器学习Tensorflow笔记4:iOS通过Core ML使用Tensorflow训练模型

Tensorflow是Google推出的人工智能框架,而Core ML是苹果推出的人工智能框架,两者是有很大的区别,其中Tensorflow是包含了训练模型和评估模型,Core ML只支持在设备上评估模型,不能训练模型...

ImWiki
05/16
0
0
Core Data 学习笔记 常用链接整理

目录 Core Data 学习笔记(一)框架简介 Core Data 学习笔记(二)被管理对象模型 Core Data 学习笔记(三)被管理对象 Core Data 学习笔记 应用示例 Core Data 学习笔记 常用链接整理 文章 ...

灰大羊
2015/07/31
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

对基于深度神经网络的Auto Encoder用于异常检测的一些思考

一、前言 现实中,大部分数据都是无标签的,人和动物多数情况下都是通过无监督学习获取概念,故而无监督学习拥有广阔的业务场景。举几个场景:网络流量是正常流量还是攻击流量、视频中的人的...

冷血狂魔
20分钟前
0
0
并发设计之A系统调用B系统

A-->B A在发送请求之前,用乐观锁,减少对B的重复调用,这样一定程度上是幂等性。 比如A系统支付功能,要调用B系统进行支付操作,但是前端对"支付"按钮不进行控制,即用户会不断多次点击支付...

汉斯-冯-拉特
40分钟前
0
0
HTTP协议通信原理

了解HTTP HTTP(HyperText Transfer Protocol)是一套计算机通过网络进行通信的规则。计算机专家设计出HTTP,使HTTP客户(如Web浏览器)能够从HTTP服务器(Web服务器)请求信息和服务。 HTTP使用...

寰宇01
今天
0
0
【Java动态性】之反射机制

一、Java反射机制简介

谢余峰
今天
1
0
Centos 6.X 部署环境搭建

1.Linux学习笔记CentOS 6.5(一)--CentOS 6.5安装过程

IT追寻者
今天
0
0
博客即同步至腾讯云+社区声明

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=8vy9bsmadbko...

xiaoge2016
今天
1
0
大数据教程(3.1):Linux系统搭建网络YUM源服务器

博主在前面的2.5章节讲述了linux系统本地YUM服务器的搭建和httpd轻量级静态网站服务器的安装,本节博主将为大家分享内网环境中搭建自己的网络YUM服务器的全过程。如果大家对本地YUM服务器还不...

em_aaron
今天
1
0
蚂蚁技术专家:一篇文章带你学习分布式事务

小蚂蚁说: 分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在这几年越来越火的微服务架构中,几乎可以说是无法避免,本文就围绕分布式事务...

Java大蜗牛
今天
1
0
新的Steam应用将拓展服务项目

导读 未来几周,Steam将推出两个免费的应用程序Steam Link和Steam Video。这两个应用程序都旨在拓展Steam平台的业务和便利性。 即将开放的Steam Link应用程序最先提供了Android测试版,它将允...

问题终结者
今天
0
0
golang 第三方包的使用总结

golang 第三方包的安装的方法: 1. go get 安装 $ go get github.com/gin-gonic/gin 注意:执行go get 命令需要先安装git命令,并配置git全局变量。 2. 源码包安装 由于国内网络问题,很多时...

科陆李明
今天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部