Mantle README

The project's README from github.com/Mantle/Mantle. As one markdown file: README.md.

Mantle

Carthage compatible CocoaPods Compatible SPM compatible

Mantle makes it easy to write a simple model layer for your Cocoa or Cocoa Touch application.

The Typical Model Object

What's wrong with the way model objects are usually written in Objective-C?

Let's use the GitHub API for demonstration. How would one typically represent a GitHub issue in Objective-C?

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

Whew, that's a lot of boilerplate for something so simple! And, even then, there are some problems that this example doesn't address:

Why Not Use Core Data?

Core Data solves certain problems very well. If you need to execute complex queries across your data, handle a huge object graph with lots of relationships, or support undo and redo, Core Data is an excellent fit.

It does, however, come with a couple of pain points:

If you're just trying to access some JSON objects, Core Data can be a lot of work for little gain.

Nonetheless, if you're using or want to use Core Data in your app already, Mantle can still be a convenient translation layer between the API and your managed model objects.

MTLModel

Enter MTLModel. This is what GHIssue looks like inheriting from 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

Notably absent from this version are implementations of <NSCoding>, <NSCopying>, -isEqual:, and -hash. By inspecting the @property declarations you have in your subclass, MTLModel can provide default implementations for all these methods.

The problems with the original example all happen to be fixed as well:

There's no way to update a GHIssue with new data from the server.

MTLModel has an extensible -mergeValuesForKeysFromModel: method, which makes it easy to specify how new model data should be integrated.

There's no way to turn a GHIssue back into JSON.

This is where reversible transformers really come in handy. +[MTLJSONAdapter JSONDictionaryFromModel:error:] can transform any model object conforming to <MTLJSONSerializing> back into a JSON dictionary. +[MTLJSONAdapter JSONArrayFromModels:error:] is the same but turns an array of model objects into an JSON array of dictionaries.

If the interface of GHIssue changes down the road, existing archives might break.

MTLModel automatically saves the version of the model object that was used for archival. When unarchiving, -decodeValueForKey:withCoder:modelVersion: will be invoked if overridden, giving you a convenient hook to upgrade old data.

MTLJSONSerializing

In order to serialize your model objects from or into JSON, you need to implement <MTLJSONSerializing> in your MTLModel subclass. This allows you to use MTLJSONAdapter to convert your model objects from JSON and back:

NSError *error = nil;
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
NSError *error = nil;
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:user error:&error];

+JSONKeyPathsByPropertyKey

The dictionary returned by this method specifies how your model object's properties map to the keys in the JSON representation, for example:


@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

In this example, the XYUser class declares four properties that Mantle handles in different ways:

Use -[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:] if your model's superclass also implements MTLJSONSerializing to merge their mappings.

If you'd like to map all properties of a Model class to themselves, you can use the +[NSDictionary mtl_identityPropertyMapWithModel:] helper method.

When deserializing JSON using +[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:], JSON keys that don't correspond to a property name or have an explicit mapping are ignored:

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];

Here, the plan would be ignored since it neither matches a property name of XYUser nor is it otherwise mapped in +JSONKeyPathsByPropertyKey.

+JSONTransformerForKey:

Implement this optional method to convert a property from a different type when deserializing from JSON.

+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key {
    if ([key isEqualToString:@"createdAt"]) {
        return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName];
    }

    return nil;
}

key is the key that applies to your model object; not the original JSON key. Keep this in mind if you transform the key names using +JSONKeyPathsByPropertyKey.

For added convenience, if you implement +<key>JSONTransformer, MTLJSONAdapter will use the result of that method instead. For example, dates that are commonly represented as strings in JSON can be transformed to NSDates like so:

    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];
    }];
}

If the transformer is reversible, it will also be used when serializing the object into JSON.

+classForParsingJSONDictionary:

If you are implementing a class cluster, implement this optional method to determine which subclass of your base class should be used when deserializing an object from 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 will then pick the class based on the JSON dictionary you pass in:

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];

Persistence

Mantle doesn't automatically persist your objects for you. However, MTLModel does conform to <NSCoding>, so model objects can be archived to disk using NSKeyedArchiver.

If you need something more powerful, or want to avoid keeping your whole model in memory at once, Core Data may be a better choice.

System Requirements

Mantle supports the following platform deployment targets:

Importing Mantle

Manually

To add Mantle to your application:

  1. Add the Mantle repository as a submodule of your application's repository.
  2. Run git submodule update --init --recursive from within the Mantle folder.
  3. Drag and drop Mantle.xcodeproj into your application's Xcode project.
  4. On the "General" tab of your application target, add Mantle.framework to the "Embedded Binaries".

If you’re instead developing Mantle on its own, use the Mantle.xcworkspace file.

Carthage

Simply add Mantle to your Cartfile:

github "Mantle/Mantle"

CocoaPods

Add Mantle to your Podfile under the build target they want it used in:

target 'MyAppOrFramework' do
  pod 'Mantle'
end

Then run a pod install within Terminal or the CocoaPods app.

Swift Package Manager

If you are writing an application, add Mantle to your project dependencies directly within Xcode.

If you are writing a package that requires Mantle as dependency, add it to the dependencies list in its Package.swift manifest, for example:

dependencies: [
    .package(url: "https://github.com/Mantle/Mantle.git", .upToNextMajor(from: "2.0.0"))
]

License

Mantle is released under the MIT license. See LICENSE.md.

More Info

Have a question? Please open an issue!

Mantle (Mantle/Mantle)

Searching Database…

11.2K ·GitHub
这是什么?Mantle 是一个适用于 Cocoa 和 Cocoa Touch 的模型框架,让你编写模型对象时无需再手写相等性比较、哈希、描述以及 JSON 转换等常规样板代码。
这是一个需要我继承的框架,还是更像一个工具库?它是一个框架:你需要让自己的模型对象继承 MTLModel,并可选择实现 MTLJSONSerializing 协议来进行 JSON 映射。
如何把 Mantle 添加到我的项目中?你可以手动将其作为 git 子模块添加,并把 Mantle.xcodeproj 拖入你的项目;也可以使用 Carthage、CocoaPods 或 Swift Package Manager。
它需要哪些苹果平台和系统版本?Mantle 支持 macOS 10.10+、iOS 9.0+、tvOS 9.0+ 和 watchOS 2.0+。
Mantle 能帮我把数据保存到磁盘吗?不能,Mantle 本身不负责持久化对象;MTLModel 遵循 NSCoding,因此你可以将对象归档到磁盘,但如果存储需求比较复杂,Core Data 可能更合适。
Mantle 如何把模型对象转回 JSON?单个对象使用 +[MTLJSONAdapter JSONDictionaryFromModel:error:],一组对象使用 +[MTLJSONAdapter JSONArrayFromModels:error:],它们会把模型转换回 JSON 字典。
如何把名称不同的 JSON 键映射到我的模型属性上?实现 +JSONKeyPathsByPropertyKey 来指定映射关系,例如把驼峰命名的属性映射为下划线命名的 JSON 键,或者将某个属性标记为不参与序列化。
能否自动把日期字符串之类的值转换为原生类型?可以,实现可选的 +JSONTransformerForKey: 方法(或 +propertyNameJSONTransformer)即可;如果转换器是可逆的,在序列化回 JSON 时也会使用它。
为什么不直接用 Core Data?Core Data 更适合复杂查询、大型对象图以及撤销/重做,但它需要大量样板代码和配置;Mantle 则是一个更轻量的转换层,适用于较简单的、以 JSON 为基础的模型,甚至可以和 Core Data 一起使用。
Creator
Mantle
Size
1.6 MB
Stars
11.2K
Age
14y
Commits · 90 Days