Mantle--国外程序员最常用的iOS模型&字典转换框架
来源:互联网 发布:淘宝客服销售案例 编辑:程序博客网 时间:2024/06/05 23:54
Mantle简介
Mantle是iOS和Mac平台下基于Objective-C编写的一个简单高效的模型层框架。
Mantle能做什么
Mantle可以轻松把JSON数据、字典(Dictionary)和模型(即Objective对象)之间的相互转换,支持自定义映射,并且内置实现了NSCoding和NSCoping,大大简化归档操作。
为什么要使用Mantle
传统的模型层方案遇到的问题
通常我们用Objective-C写的模型层遇到了什么问题?
我们可以用Github API来举例。现在假设我们想用Objective-C展现一个Github Issue,应该怎么做?
目前我们可以想到
直接解析JSON数据字典,然后展现给UI
将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
你会看到,如此简单的事情却有很多弊端。甚至,还有一些其他问题,这个例子里面没有展示出来。
无法使用服务器的新数据来更新这个
GHIssue
无法反过来将
GHIssue
转换成JSON
对于
GHIssueState
,如果枚举改编了,现有的归档会崩溃如果
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
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共同论道!
- Mantle--国外程序员最常用的iOS模型&字典转换框架
- Mantle--国外程序员最常用的iOS模型&字典转换框架
- iOS Mantle解析框架简单的使用入门(字典映射model)
- 模型映射库Mantle,高效ios程序员都应该使用
- 模型转换工具Mantle, MJExtension, JSONModel的使用和异同
- 模型转换工具Mantle, MJExtension, JSONModel的使用和异同
- iOS的Mantle实战
- iOS的Mantle实战
- 最常用的国外杀毒软件
- JSON和模型之间最快捷方便的转换iOS框架:MJExtension
- IOS 字典快速转换为Model 模型
- IOS将字典转换成模型对象
- IOS 字典模型互转框架 MJExtension
- 国外最流行的PHP MVC框架
- 模型转换工具 Mantle, MJExtension, JSONModel 的使用和异同及防崩溃能力比较
- MJExtension字典和模型之间相互转换的轻量级框架的使用实例
- Java程序员最常用的8个Java日志框架
- Java程序员最常用的8个Java日志框架
- Qt model/View
- Android onInterceptTouchEvent与onTouchEvent调用关系
- xcode 真机调试报错(一)
- 单例模式-Singleton Pattern 确保对象的唯一性——单例模式 (二):负载均衡器的设计与实现
- 数据结构--基本排序算法(js版)
- Mantle--国外程序员最常用的iOS模型&字典转换框架
- RKSwipeBetweenViewControllers
- android 使用到的各种工具类
- 用户和用户组进阶命令
- iOS防止Button连续点击
- 微信抢红包插件之Accessibilityservice学习
- Qt文件read 写
- 色值的透明度与十六进制代码转换
- php获取当前月的所有日期