Mantle 使您可以轻松地为 Cocoa 或 Cocoa Touch 应用程序编写简单的模型层。
通常用 Objective-C 编写模型对象的方式有什么问题?
我们使用GitHub API进行演示。通常如何用 Objective-C 表示 GitHub 问题?
typedef 枚举:NSUInteger { GHIssueState打开, GHI问题状态已关闭 } GHIssueState;@interface GHIssue : NSObject <NSCoding, NSCopying>@property (非原子,复制,只读) NSURL *URL;@property (非原子,复制,只读) NSURL *HTMLURL;@property (非原子,复制,只读) NSNumber * number;@property(非原子、分配、只读) GHIssueState 状态;@property (非原子、复制、只读) NSString *reporterLogin;@property (非原子、复制、只读) NSDate *updatedAt;@property (非原子、强、只读) GHUser *受让人;@property (非原子、复制、只读) NSDate *retrievedAt; @property (非原子,复制) NSString *title;@property (非原子,复制) NSString *body; - (id)initWithDictionary:(NSDictionary *)字典;@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 *)字典 { self = [self init];if (self == nil) 返回 nil; _URL = [NSURL URLWithString:字典[@"url"]]; _HTMLURL = [NSURL URLWithString:字典[@"html_url"]]; _number = 字典[@"number"];if ([字典[@"state"] isEqualToString:@"open"]) { _state = GHIssueStateOpen; } else if ([字典[@"state"] isEqualToString:@"close"]) { _state = GHIssueStateClosed; } _title = [字典[@"title"]复制]; _retrievedAt = [NSDate 日期]; _body = [字典[@"body"]复制]; _reporterLogin = [字典[@"用户"][@"登录"]复制]; _assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]]; _updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];返回自我; } - (id)initWithCoder:(NSCoder *)编码器 { self = [self init];if (self == nil) 返回 nil; _URL = [编码器decodeObjectForKey:@"URL"]; _HTMLURL = [编码器decodeObjectForKey:@"HTMLURL"]; _number = [编码器decodeObjectForKey:@"number"]; _state = [编码器decodeUnsignedIntegerForKey:@"state"]; _title = [编码器decodeObjectForKey:@"标题"]; _retrievedAt = [NSDate 日期]; _body = [编码器decodeObjectForKey:@“body”]; _reporterLogin = [编码器decodeObjectForKey:@“reporterLogin”]; _assignee = [编码器decodeObjectForKey:@“受让人”]; _updatedAt = [编码器decodeObjectForKey:@"updatedAt"];返回自身; } - (void)encodeWithCoder:(NSCoder *)coder {if (self.URL != nil) [编码器encodeObject:self.URL forKey:@"URL"];if (self.HTMLURL != nil) [编码器encodeObject:self .HTMLURL forKey:@"HTMLURL"];if (self.number != nil) [编码器encodeObject:self.number forKey:@"number"];if (self.title != nil) [编码器encodeObject:self.title forKey:@"title"];if (self.body != nil) [编码器encodeObject:self.body forKey:@"body"];if (self.reporterLogin != nil) [编码器encodeObject:self.reporterLogin forKey:@"reporterLogin"];if (self.assignee != nil) [编码器encodeObject:self.assignee forKey:@"assignee"];if (self.updatedAt != nil) [编码器encodeObject:self.updatedAt forKey:@"updatedAt"]; [编码器encodeUnsignedInteger:self.state forKey:@"state"]; } - (id)copyWithZone:(NSZone *)区域 { GHIssue *issue = [[self.class allocWithZone:zone] init]; 问题->_URL = self.URL; 问题->_HTMLURL = self.HTMLURL; 问题->_number = self.number; 问题->_state = self.state; 问题->_reporterLogin = self.reporterLogin; 问题->_assignee = self.assignee; 问题->_updatedAt = self.updatedAt; 问题.标题=自我.标题; 问题->_retrievedAt = [NSDate 日期]; Issue.body = self.body;返回问题; } - (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 有很多自己的样板文件。正确设置核心数据堆栈(带有持久性存储和持久性存储协调器)并执行提取可能需要多行代码。
很难做到正确。即使是经验丰富的开发人员在使用 Core Data 时也可能会犯错误,而且该框架并不宽容。
如果您只是尝试访问一些 JSON 对象,那么 Core Data 可能会耗费大量工作却收效甚微。
尽管如此,如果您已经在应用程序中使用或想要使用 Core Data,Mantle 仍然可以成为 API 和托管模型对象之间的便捷转换层。
输入MTLModel 。这就是GHIssue
继承自MTLModel
的样子:
typedef 枚举:NSUInteger { GHIssueState打开, GHI问题状态已关闭 GHIssueState;@interface GHIssue : MTLModel <MTLJSONSerializing>@property (非原子,复制,只读) NSURL *URL;@property (非原子,复制,只读) NSURL *HTMLURL;@property (非原子,复制,只读) NSNumber *number; @property(非原子、分配、只读)GHIssueState 状态;@property (非原子、复制、只读) NSString *reporterLogin;@property (非原子、强、只读) GHUser *受让人;@property (非原子、复制、只读) NSDate *updatedAt;@property (非原子、复制) NSString *title;@property (非原子,复制) NSString *body;@property (非原子,复制,只读) NSDate *检索于;@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”,@“受让人”:@“受让人”,@“updatedAt”:@“updated_at”}; } + (NSValueTransformer *)URLJSONTransformer {return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; } + (NSValueTransformer *)HTMLURLJSONTransformer {return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; } + (NSValueTransformer *)stateJSONTransformer {return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{@"open": @(GHIssueStateOpen),@"close": @(GHIssueStateClosed) }]; } + (NSValueTransformer *)assigneeJSONTransformer {return [MTLJSONAdapter DictionaryTransformerWithModelClass:GHUser.class]; } + (NSValueTransformer *)updatedAtJSONTransformer {return [MTLValueTransformer TransformerUsingForwardBlock:^id(NSString *dateString, BOOL *成功, 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 错误:(NSError **)error { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil;// 存储一个初始化时需要在本地确定的值。 _retrievedAt = [NSDate date];return self; }@结尾
值得注意的是,此版本中缺少<NSCoding>
、 <NSCopying>
、 -isEqual:
和-hash
的实现。通过检查子类中的@property
声明, MTLModel
可以为所有这些方法提供默认实现。
原始示例的问题也全部得到解决:
无法使用服务器中的新数据更新
GHIssue
。
MTLModel
有一个可扩展的-mergeValuesForKeysFromModel:
方法,可以轻松指定如何集成新模型数据。
无法将
GHIssue
转回JSON。
这就是可逆变压器真正派上用场的地方。 +[MTLJSONAdapter JSONDictionaryFromModel:error:]
可以将任何符合<MTLJSONSerializing>
的模型对象转换回 JSON 字典。 +[MTLJSONAdapter JSONArrayFromModels:error:]
是相同的,但将模型对象数组转换为 JSON 字典数组。
如果
GHIssue
的界面日后发生变化,现有档案可能会损坏。
MTLModel
自动保存用于存档的模型对象的版本。解档时,如果覆盖-decodeValueForKey:withCoder:modelVersion:
将被调用,为您提供一个方便的挂钩来升级旧数据。
为了将模型对象从 JSON 序列化或序列化为 JSON,您需要在MTLModel
子类中实现<MTLJSONSerializing>
。这允许您使用MTLJSONAdapter
将模型对象与 JSON 相互转换:
NSError *错误= nil; XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
NSError *error = nil;NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:用户错误:&error];
+JSONKeyPathsByPropertyKey
此方法返回的字典指定模型对象的属性如何映射到 JSON 表示形式中的键,例如:
@interface XYUser : MTLModel@property (只读,非原子,复制) NSString *name;@property (只读,非原子,强) NSDate *createdAt;@property (只读,非原子,分配,getter = isMeUser) BOOL meUser;@property (只读、非原子、强) XYHelper *helper;@end@implementation XYUser+ (NSDictionary *)JSONKeyPathsByPropertyKey {return @{@"name": @"name",@"createdAt": @"created_at"}; } - (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue 错误:(NSError **)error { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil; _helper = [XYHelper helperWithName:self.name createAt:self.createdAt];返回自我; }@结尾
在此示例中, XYUser
类声明了 Mantle 以不同方式处理的四个属性:
name
映射到 JSON 表示中的同名键。
createdAt
被转换为它的蛇形大小写等效项。
meUser
未序列化为 JSON。
helper
在 JSON 反序列化后初始化一次。
如果模型的超类也实现了MTLJSONSerializing
来合并它们的映射,请使用-[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]
。
如果您想将 Model 类的所有属性映射到自身,可以使用+[NSDictionary mtl_identityPropertyMapWithModel:]
帮助器方法。
使用+[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]
反序列化 JSON 时,与属性名称不对应或具有显式映射的 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
将被忽略,因为它既不匹配XYUser
的属性名称,也不会以其他方式映射到+JSONKeyPathsByPropertyKey
中。
+JSONTransformerForKey:
实现此可选方法以在从 JSON 反序列化时将属性从不同类型转换。
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key { if ([key isEqualToString:@"createdAt"]) { return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName]; } return nil; }
key
是适用于您的模型对象的键;不是原始的 JSON 密钥。如果您使用+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]; }]; }
如果转换器是可逆的,则在将对象序列化为 JSON 时也会使用它。
+classForParsingJSONDictionary:
如果您正在实现类集群,请实现此可选方法来确定在从 JSON 反序列化对象时应使用基类的哪个子类。
@interface XYMessage : MTLModel@end@interface XYTextMessage: XYMessage@property (只读、非原子、复制) NSString *body;@end@interface XYPictureMessage : XYMessage@property (只读、非原子、强) NSURL *imageURL;@end@implementation XYMessage+ (类)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {if (JSONDictionary[@"image_url"] != nil) {return XYPictureMessage.class; }if (JSONDictionary[@"body"] != nil) {return XYTextMessage.class; }NSAssert(NO, @"JSON 字典'%@'没有匹配的类。", JSONDictionary);return self; }@结尾
然后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 可能是更好的选择。
Mantle支持以下平台部署目标:
macOS 10.10+
iOS 9.0+
电视操作系统 9.0+
watchOS 2.0+
要将 Mantle 添加到您的应用程序中:
将 Mantle 存储库添加为应用程序存储库的子模块。
从 Mantle 文件夹中运行git submodule update --init --recursive
。
将Mantle.xcodeproj
拖放到应用程序的 Xcode 项目中。
在应用程序目标的“常规”选项卡上,将Mantle.framework
添加到“嵌入式二进制文件”中。
如果您要自行开发 Mantle,请使用Mantle.xcworkspace
文件。
只需将 Mantle 添加到您的Cartfile
中即可:
github "Mantle/Mantle"
将 Mantle 添加到Podfile
中他们希望使用它的构建目标下:
target 'MyAppOrFramework' do pod 'Mantle' end
然后在终端或 CocoaPods 应用程序中运行pod install
。
如果您正在编写应用程序,请直接在 Xcode 中将 Mantle 添加到项目依赖项中。
如果您正在编写需要 Mantle 作为依赖项的包,请将其添加到其Package.swift
清单中的dependencies
列表中,例如:
dependencies: [ .package(url: "https://github.com/Mantle/Mantle.git", .upToNextMajor(from: "2.0.0")) ]
Mantle 是在 MIT 许可下发布的。请参阅 LICENSE.md。
有问题吗?请打开一个问题!