Mantle 2.2.0

Mantle 2.2.0

测试已测试
语言语言 Obj-CObjective C
许可 NOASSERTION
发布最后发布2021年12月

Robert BöhnkeDavid CauntJan GormanMantle GitHub Actions维护。



Mantle 2.2.0

  • Robert Böhnke,David Caunt 和 Jan Gorman

Mantle

Carthage compatible CocoaPods Compatible SPM compatible Platform

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 添加到您的应用程序中

  1. 将 Mantle 代码库作为子模块添加到应用程序代码库中。
  2. 在 Mantle 文件夹内部运行 git submodule update --init --recursive
  3. Mantle.xcodeproj 拖放到您的应用程序 Xcode 项目中。
  4. 在应用程序目标的“常规”选项卡中,将 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

更多信息

有问题?请 新建问题