MantleObjC 2.1.1

MantleObjC 2.1.1

Hai Feng Kao 维护。



  • 作者
  • Hai Feng Kao

Mantle Carthage compatible

Mantle 使编写 Cocoa 或 Cocoa Touch 应用程序的简单模型层变得容易。

典型的模型对象

通常在 Objective-C 中编写模型对象的方式有问题吗?

让我们使用 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?

Core Data 非常擅长解决某些问题。如果您需要在数据中执行复杂的查询、处理具有许多关系的巨大对象图或支持撤销和重做,Core Data 是一个很好的选择。

然而,它还是存在一些痛点。

  • 仍然有许多模板代码。 管理对象减少了一些上述代码中的模板代码,但Core Data本身也有很多。正确设置Core Data堆栈(带持久存储和持久存储协调器)以及执行查询可能需要许多行代码。
  • 很难做对。 即使有经验的开发者在使用Core Data时也可能出错,并且该框架并不宽容。

如果你只是尝试访问一些JSON对象,Core Data可能是个大工程,收获却很少。

尽管如此,如果你已经在你的应用中使用或想使用Core Data,Mantle仍然可以作为API和你的管理模型对象之间方便的转换层。

MTLModel

这是继承自 MTLModelGHIssue 的样子。

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:]` 可以将遵循 `` 的任何模型对象转换回JSON字典。`+[MTLJSONAdapter JSONArrayFromModels:error:]` 与此相同,但它将模型对象的数组转换为JSON字典数组。

如果 GHIssue 的接口在以后发生变化,现有的存档可能会损坏。

MTLModel 会自动保存为存档所使用的模型对象的版本。在解档时,如果重写了,则会调用 -decodeValueForKey:withCoder:modelVersion:,这为你提供了一个方便的钩子来升级旧数据。

MTLJSONSerializing

为了将你的模型对象从或转换为JSON,你需要在 MTLModel 子类中实现 ``。这允许你使用 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被转换为蛇形(下划线)等价的键。
  • meUser不会被序列化到JSON中。
  • helper在JSON反序列化后恰好初始化一次。

如果你的模型的上层类也实现了MTLJSONSerializing,则使用-[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]来合并它们的映射。

如果你想将模型类的所有属性映射为自己,可以使用+[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>JSONTransformerMTLJSONAdapter将使用该方法的输出。例如,在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 支持 OS X 10.9+ 和 iOS 8.0+。

导入 Mantle

要将 Mantle 添加到您的应用程序

  1. 将 Mantle 仓库添加为您的应用程序仓库的子模块。
  2. 在 Mantle 文件夹内运行 git submodule update --init --recursive
  3. Mantle.xcodeproj 拖放到您应用程序的 Xcode 项目中。
  4. 在应用程序目标的 "General" 选项卡中,将 Mantle.framework 添加到 "Embedded Binaries"。

Carthage 用户只需要将 Mantle 添加到他们的 Cartfile

github "Mantle/Mantle"

如果您想使用 CocoaPods,有些由第三方贡献的 Mantle podspecs

如果您正在单独开发 Mantle,请使用 Mantle.xcworkspace 文件。

许可

Mantle 在 MIT 许可下发布。见 LICENSE.md

更多信息

有问题?请 提交问题