Mantle
Mantle让编写Cocoa或Cocoa Touch应用程序的简单模型层变得容易。
典型的模型对象
通常编写模型对象的这种方式有什么问题?
让我们以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];
}
@end
哇,这么简单的东西却有很多样板代码!而且,即使这样,也存在一些示例没有解决的问题
- 没有方法可以使用服务器的新数据更新一个
GHIssue
。 - 没有方法可以将一个
GHIssue
转换回JSON。 GHIssueState
应该被直接编码。如果枚举在未来发生变化,现有的存档可能会损坏。- 如果将来的
GHIssue
接口发生变化,现有的存档可能会损坏。
为什么不使用Core Data?
_coreData_解决了一些问题非常出色。如果您需要在数据上执行复杂的查询、处理包含大量关系的庞大对象图或支持撤销和重做,_coreData_是完美的选择。
但是,也存在一些痛点
- 仍然有很多样板代码。托管对象减少了上述部分样板代码,但_coreData_本身还有很多。正确设置_coreData_堆栈(包括持久存储和持久存储协调器),以及执行检索需要很多行代码。
- 很难做到完美。即使有经验的开发者使用_coreData_时也可能出错,并且该框架并不宽容。
如果您只是尝试访问一些JSON对象,_coreData_可能需要做很多工作却收获不多。
尽管如此,如果您已经在应用程序中使用或想要使用_coreData_,_Mantle_仍可以作为API和托管模型对象之间方便的转换层的便利选择。
MTLModel
请看以下内容:MTLModel。这是从GHIssue
继承而来的样子
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
的实现。通过检查您在子类中声明的@property
声明,_MTLModel_可以提供所有这些方法的标准实现。
原始示例中遇到的所有问题都得到了解决
没有方法可以使用服务器的新数据更新一个
GHIssue
。
MTLModel
具有可扩展的-mergeValuesForKeysFromModel:
方法,这使得指定如何集成新的模型数据变得容易。
没有方法可以将一个
GHIssue
转换回JSON。
这正是可逆转换器真正有用之处。使用+[MTLJSONAdapter JSONDictionaryFromModel:error:]
可以将符合<MTLJSONSerializing>
的任何模型对象转换回JSON字典。使用+[MTLJSONAdapter JSONArrayFromModels:error:]
可以达到同样的效果,但它将模型对象的数组转换成含有字典的JSON数组。
如果将来的
GHIssue
接口发生变化,现有的存档可能会损坏。
MTLModel
会自动保存用于存档的模型对象版本。在存档时,如果覆盖了-decodeValueForKey:withCoder:modelVersion:
,则会调用它,这为您提供了升级旧数据的便利钩子。
MTLJSONSerializing
为了将模型对象从JSON序列化或将其序列化到JSON,您需要在MTLModel
子类中实现<MTLJSONSerializing>
。这允许您使用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
转换为它的Snake case等价物。meUser
不序列化到JSON。helper
在JSON反序列化后在一次初始化。
如果您的模型父类也实现了MTLJSONSerializing
,请使用-[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]
合并它们的映射。
如果您希望将Model类的所有属性映射为自己,可以使用+[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
会被忽略,因为它既不匹配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 (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可能是一个更好的选择。
系统要求
Mantle支持以下平台部署目标
- macOS 10.10+
- iOS 9.0+
- tvOS 9.0+
- watchOS 2.0+
导入 Mantle
手动
要将 Mantle 添加到您的应用程序中
- 将 Mantle 代码库作为子模块添加到应用程序代码库中。
- 在 Mantle 文件夹内部运行
git submodule update --init --recursive
。 - 将
Mantle.xcodeproj
拖放到您的应用程序 Xcode 项目中。 - 在应用程序目标的“常规”选项卡中,将
Mantle.framework
添加到“嵌入的二进制文件”。
如果您是在单独开发 Mantle,则使用Mantle.xcworkspace
文件。
Carthage
只需将 Mantle 添加到您的 Cartfile
github "Mantle/Mantle"
CocoaPods
将 Mantle 添加到您的 Podfile
中,位置是在想要使用 Mantle 的构建目标下。
target 'MyAppOrFramework' do
pod 'Mantle'
end
然后,在终端或 CocoaPods 应用 中运行 pod install
。
Swift 包管理器
如果您正在编写应用程序,请直接在 Xcode 中将 Mantle 添加到您的项目依赖项中,请参考 Xcode 中的添加包依赖项。
如果您正在编写一个需要 Mantle 作为依赖项的包,请在它的 Package.swift
清单文件中将它添加到 dependencies
列表中,例如
dependencies: [
.package(url: "https://github.com/Mantle/Mantle.git", .upToNextMajor(from: "2.0.0"))
]
许可证
Mantle 在 MIT 许可下发布。参见 LICENSE.md。
更多信息
有问题?请 新建问题!