Giter Site home page Giter Site logo

mantle's Introduction

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:

  • There's no way to update a GHIssue with new data from the server.
  • There's no way to turn a GHIssue back into JSON.
  • GHIssueState shouldn't be encoded as-is. If the enum changes in the future, existing archives might break.
  • If the interface of GHIssue changes down the road, existing archives might break.

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:

  • There's still a lot of boilerplate. Managed objects reduce some of the boilerplate seen above, but Core Data has plenty of its own. Correctly setting up a Core Data stack (with a persistent store and persistent store coordinator) and executing fetches can take many lines of code.
  • It's hard to get right. Even experienced developers can make mistakes when using Core Data, and the framework is not forgiving.

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:

  • name is mapped to a key of the same name in the JSON representation.
  • createdAt is converted to its snake case equivalent.
  • meUser is not serialized into JSON.
  • helper is initialized exactly once after JSON deserialization.

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:

  • macOS 10.10+
  • iOS 9.0+
  • tvOS 9.0+
  • watchOS 2.0+

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.

Simply add Mantle to your Cartfile:

github "Mantle/Mantle"

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.

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's People

Contributors

abotkin-cpi avatar adamkaplan avatar ahti avatar akashivskyy avatar bartvandendriessche avatar bigboybad avatar boidanny avatar dcaunt avatar defagos avatar hengw avatar ikesyo avatar indragiek avatar jilouc avatar jmah avatar joshaber avatar joshvera avatar jspahrsummers avatar kilink avatar kpmaalej avatar kylef avatar mdiep avatar mickeyreiss avatar paulyoung avatar petrmifek avatar rastersize avatar rex-remind101 avatar robb avatar robrix avatar terinjokes avatar yhkaplan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mantle's Issues

Flat JSON to nested object graph

Assuming the following JSON:

{
    "username": "asdf",
    "firstname": "John",
    "lastname": "Doe"
}

How can I map this into an object graph that looks like:

@interface User : MTLModel
@property(nonatomic) NSString *firstname;
@property(nonatomic) NSString *lastname;
@property(nonatomic) Credentials *credentials;
@end

// --------
@interface Credentials : MTLModel
@property(nonatomic) NSString *username;
@end

Converting this to JSON was pretty straightforward. But I couldn't find a way to reverse converting this flat structure into the nested object graph, if this is possible at all (I hope so :)).

I'm doing the to-JSON like so:

@implementation User

+ (NSDictionary *) JSONKeyPathsByPropertyKey
{
    return @{ @"firstname": @"firstname", @"lastname": @"lastname", @"credentials.username": @"username" };
}

@end 


// --------
@implementation Credentials

+ (NSDictionary *) JSONKeyPathsByPropertyKey
{
    return @{ @"username": @"username" };
}

@end 

Exclude properties from being part of the external representation ?

Hi,

imagine I have the following 'model':

@property(strong, nonatomic) NSNumber *recId;
@property(copy, nonatomic) NSString *title;
@property(copy, nonatomic) NSString *style;

@property(strong, nonatomic) UIColor *color;

For what ever reason the UIColor is part of it... is there a way to exclude the "color" property ? I don't want to have it being part of the "map representation"

Deserialising into NSManagedObjects restores the parent abstract class, not the instance class

I have a feeling this might turn into a feature request, so I've filed this issue rather than posting to Stack Overbleh.

Given the following Core Data entity structure:

  • Page (abstract)
    • PageType1
    • PageType2

I've successfully serialised this to JSON, however deserializing into managed objects again restores the parent abstract class, and not the child entity classes (I get Page instead of PageType1 or PageType2).

I've implemented the following:

+ (Class)classForDeserializingManagedObject:(NSManagedObject *)managedObject
{
    if ([managedObject isKindOfClass:[PageType1 class]]) {
          return [MantlePageType1 class];
    } else if ([managedObject isKindOfClass:[PageType2 class]]) {
          return [MantlePageType2 class];
    }

     return [self class];
}

However the problem remains as described.

Is it possible to control this? Am I missing something?

Add support for NSSecureCoding

It would be fantastic if Mantle had support for NSSecureCoding, which conforms to NSCoding but provides an adder layer of protection against object substitution attacks. It is primarily used for transferring objects between an XPC service and its host application. Only one major change is required to support NSSecureCoding, which is that the object's class needs to be known before actually decoding it. Hence, the decodeObjectOfClass:forKey: method of NSCoder must be used instead of decodeObjectForKey:.

I took a look at MTLModel to see how this could be implemented, and it seems that it wouldn't be too difficult considering that the ext_propertyAttributes struct has an objectClass member. Of course, this wouldn't work if the type was 'id', but I suppose the NSSecureCoding implementation could be an option on MTLModel (vs. being the default) and throw an exception if a property with an id type is encountered.

I can go ahead and attempt an implementation some time next week, just wondering if you guys have any thoughts on this before I start working on it.

Make it easy to create class clusters for models and view models

Class clusters are useful in a model layer to represent different data types that have some commonalities, like GitHub's own events API. It'd be useful to have something in Mantle that can set up such a structure for you.

Maybe Mantle could support a registration concept a la NSURLProtocol, so that the abstract base class doesn't have to know about the concrete subclasses. Or maybe it could just do crazy runtime things to find subclasses and know how to use them.

Either way, whatever gets implemented shouldn't be specific to MTLModel, because this would also be useful for view models.

Implementation of isEqual: fails with NSDate objects due to sub-seconds differences

Hi,

The actual implementation of isEqual: does not work with NSDate properties due to sub-seconds differences between dates. In fact, the NSDate class provides a method to deal with this situation: isEqualToDate:.

Wouldn't be nice to introduce this check?

for (NSString *key in self.class.propertyKeys) {
  id selfValue = [self valueForKey:key];
  id modelValue = [model valueForKey:key];

  SEL equalitySelector = NULL;
  if ([selfValue isKindOfClass:[NSDate class]]) {
    equalitySelector = @selector(isEqualToDate:);
  } else {
    equalitySelector = @selector(isEqual:);
  }

  BOOL valuesEqual = ((selfValue == nil && modelValue == nil) || [selfValue performSelector:equalitySelector withObject:modelValue]);
  if (!valuesEqual) return NO;
}

Moreover, since isEqualToString: is faster than isEqual: for NSString objects this case could be added too.

If you think this is worth it I can send a pull request.

nested (MTLModel) classes

Having the following object model:

A simple employee class:

@interface MWEmployee : MTLModel

@property (nonatomic, copy) NSString* name;
@property (nonatomic, copy) NSArray* tasks;

@end

And employees can have tasks:

@interface MWTask : MTLModel

@property (nonatomic, copy) NSString* title;
@property (nonatomic, copy) NSString* desc;

@end

Now let's build a simple object graph:

MWTask* task = [[MWTask alloc] init];
task.title = @"Some task";
task.desc = @"lot's of workz";

MWEmployee* emp = [[MWEmployee alloc] init];
emp.name = @"Mr. X";
emp.tasks = [NSArray arrayWithObject:task];

Now let's try to get some JSON:

NSData* encodedData = [NSJSONSerialization
    dataWithJSONObject:[emp externalRepresentation] // using the given NSDictionary
    options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:encodedData encoding:NSUTF8StringEncoding];

But I am getting nothing back (as JSON), the NSJSONSerialization fails and says Invalid type in JSON write (MWTask)

Are nested (MTLModel) classes not supported ?

Exclude properties from being considered regarding equality

Suppose I have some properties that hold cached data — stuff that can be derived but at a time penalty, so I'm cacheing it in the model. I'd like to exclude such properties from equality calculations. Perhaps there should be a method +(NSSet*)keypathsToExcludeFromEquality that you can override to easily exclude them?

I love that object equality is handled for me by default. It'd be even better if it was customisable in this manner!

Does Mantle handle any persistence?

Maybe I'm missing the point here, but the Mantle blog post covers nothing about actually saving data to the device. This seems to just be parsing JSON/Dictionaries into an object...am I missing something here?

MTLModel subclass encodeWithCoder -> unrecognized selector sent to instance

After updating to the latest version I suddenly have a problem archiving my MTLModel subclass object.

I'm just using tmdbItemData = [NSKeyedArchiver archivedDataWithRootObject:self.tmdbItem];

And I get the following exception:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[TMDBItem encodeWithCoder:]: unrecognized selector sent to instance 0x9e55650'

My TMDBItem header file starts like this...

import <Mantle/Mantle.h>

@interface TMDBItem : MTLModel

If I sublass encodeWithCoder in TMDBItem I still get the same error

  • (void)encodeWithCoder:(NSCoder *)coder
    {
    [super encodeWithCoder:coder]; // crashes
    }

Any ideas?

Handling Model Relationship

I've been pondering how to handle relationships between different models, especially in cases where copies of certain instances will seem to be made in memory.

For example, issues on GitHub have owners: the person who created them. But many issues will have the same owners.

Assuming the external representation includes information about the user, the naïve approach would be to use +[NSValueTransformer mtl_externalRepresentationTransformerWithModelClass:]. But in the case of many issues having a single owner, each issue would create it's own user object.

Is there a suggested alternative for this?

this class is not key value coding-compliant for the key id.

when i try to init a Question from a dict i got this error,

[<Question 0xf14eee0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key id.

Question.h

#import <Mantle.h>

@interface Question : MTLModel

@property (nonatomic) NSUInteger *questionId;
@property (nonatomic) NSString *title;
@property (nonatomic) NSNumber *authorId;
@property (nonatomic) NSDate *datePublish;
@property (nonatomic) NSDate *dateCreate;
@property (nonatomic) NSNumber *answerCount;
@property (nonatomic) NSNumber *channelId;
@property (nonatomic) BOOL *isStick;
@property (nonatomic) NSNumber *recommendedCount;
@property (nonatomic) BOOL *recommended;
@property (nonatomic) NSString *show;


@end

this is my Question.m

#import "Question.h"
#import <Mantle.h>

@implementation Question

+ (NSDictionary *)externalRepresentationKeyPathsByPropertyKey {
    return [super.externalRepresentationKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{
            @"questionId":@"id",
            @"title":@"title",
            @"authorId":@"author_id",
            @"datePublish":@"date_publish",
            @"dateCreate":@"date_create",
            @"answerCount":@"answer_count",
            @"channelId":@"channel_id",
            @"isStick":@"is_stick",
            @"userSubmit":@"user_submit",
            @"recommendCount":@"recommend_count",
            @"recommended":@"recommended",
            @"show":@"show"
            }];
}

@end

i am new to ios dev, i google a lot, but still can not figure out .

Add MTLEqualObjects()

isEqual: doesn't work if you want to consider two nil objects equal. We should add a function that can handle both objects being nil, or otherwise pass through to isEqual:.

Secure coding conformance requires implementation of -initWithCoder/-encodeWithCoder:

I ran into a bit of a headache when trying to add conformance to NSSecureCoding (as enabled by pull request #70). At first I only added the protocol to my classes and implemented +supportsSecureCoding unfortunately this resulted in a crash when Cocoa tried to serialize the object using a secure coder. After some experimentation I have found a workaround, sadly it’s not what I would call pretty. What I had to do was to also implement -initWithCoder: and -encodeWithCoder: in all of my models (see below).

Is this the intended behaviour, am I doing something wrong or have I run into a bug? I’m guessing the problem occurs because the coding support is implemented as a category on MTLModel.

@implementation MyModel
+ (BOOL)supportsSecureCoding {
    return YES;
}

- (id)initWithCoder:(NSCoder *)coder {
    self = [super initWithCoder:coder];
    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
    [super encodeWithCoder:coder];
}
@end

The error message I get when only implementing +supportsSecureCoding is:

Exception: Class 'MyModel' has a superclass that supports secure coding, but
'MyModel' overrides -initWithCoder: and does not override
+supportsSecureCoding. The class must implement +supportsSecureCoding and
return YES to verify that its implementation of -initWithCoder: is secure
coding compliant.

I have a sample project set up which I can share with you if you want.

The File "*.xccconfig" couldn't be opened because it's path couldn't be resolved. It may be missing.

Steps to reproduce:

  1. git clone git://github.com/github/Mantle.git
  2. run script/bootstrap
  3. Open up the Xcode project
  4. Lots of project integrity warnings, missing Release/Profile/Debug.xcconfig in the Mantle project, iOS-Application/Profile.xcconfig in the Mantle iOS tests, iOS-StaticLibrary/Profile.xcconfig in the Mantle iOS Lib

I can see the files are in /Mantle/libextobjc/Configuration/, and Xcode will let me view them from within the project, but for whatever reason I have to remove/re-add them to the project for Xcode to recognize them.

New name

Maverick isn't great. It's too similar to Rebel.

Some ideas:

  • Bedrock
  • Flintstone(s)
  • Shale
  • Stratum
  • Strata
  • Neutrino
  • Hadron

Must I use all the keys in the JSON when initializing?

I was getting a "this class is not key value coding-compliant for the key some_key_in_external_json" which causes a unit test to fail.

I currently have no need for that key-value pair in the JSON I get externally. I just want to ignore that when the JSON is getting parsed.

I'm a little unclear as to why it wouldn't just fail gracefully and why it seems to want me to know about it and do something about it. What if my external webservice adds to its models or provides data to multiple clients, and only some of those key-value pairs are of interest to me?

Shouldn't it only care about the mappings I define in externalRepresentationKeyPathsByPropertyKey? Do I have to explicitly say "I know about you but don't care, so your property key is [NSNull null]" ??

// for example
+ (NSDictionary *)externalRepresentationKeyPathsByPropertyKey {
    return [super.externalRepresentationKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{
            @"url"          : @"transaction_url",
            @"identifier"   : @"transaction_id",
            [NSNull null] : @"some_key_in_external_json" 
            }];
}

(PS - I'm new to Mantle but am excited to be working with it!! Please correct me if I've misunderstood something fundamental.)

Returning a C type from a transformer

I'm using mantle for reducing boilerplate with my model generation form my REST api and thus far it's working well, but I'm a bit confused about how returning C types from Transformers works (and trasnformers in general a bit, admittedly).

I'm using this to resolve a mapping between a property ("coordinate") and a dictionary returned by my API.

my code below:

+(NSValueTransformer *)coordinateTransformer
{
    return [MTLValueTransformer transformerWithBlock:^id (NSDictionary *point) {
        if (point == nil) return nil;
        return CLLocationCoordinate2DMake([[point valueForKey:@"lat"] doubleValue], [[point valueForKey:@"long"] doubleValue]);
    }];
}

Thanks, and sorry if this is a bit n00b-y

Remove higher-order functions?

The higher-order functions (map, filter, fold, etc.) that Mantle adds to collections were a minor point of contention when the framework was first open sourced. As ReactiveCocoa gets more and more capable of replacing them (and in a much more generic way), it seems reasonable to get rid of the duplicate functionality here.

Does anyone feel like they should still belong in Mantle?

How to handle handle JSON dictionaries with variable keys

Hi!

I've got a JSON structure like this

{
    "created_on": 1363863192, 
    "id": 97575, 
    "revisions": {
        "203087": {
            "created_on": 1364233838, 
            "description": "", 
            "id": 203087, 
        },
        "203084": {
            "created_on": 1364233555, 
            "description": "", 
            "id": 203084, 
        },
        "203084": {
            "created_on": 1364233222, 
            "description": "", 
            "id": 203084, 
        }
    }
}

The overall structure is:

ThingObject
Revision1
Revision2
Revision3
...

The structure is a bit unusual in that rather than revisions being an array, instead it is a JSON dictionary. The dictionary goes from id of the object -> dict containing object values.

If it was an array I could use mtl_JSONDictionaryTransformerWithModelClass to create my nested structure. Is there anything similar for handling a dictionary like this? If not is there a place during construction of the MTLModel objects that I can manually handle this?

MTLValidateAndSetValue crashes on nil value

I'm trying to create a model from a JSON dictionary, and some values contain NULL.

Location *location = [MTLJSONAdapter modelOfClass:[Location class] fromJSONDictionary:locationDic error:nil];
locationDic:
{
    address = "<null>";
    city = "<null>";
    created = "<null>";
    description = "<null>";
    "foursquare_id" = "<null>";
    id = "<null>";
    lat = "<null>";
    lng = "<null>";
    modified = "<null>";
    name = "<null>";
    phone = "<null>";
    state = "<null>";
    zip = "<null>";
}

I get a crash on line 47 in MTLModel.m on

if (forceUpdate || value != validatedValue) {
                [obj setValue:validatedValue forKey:key];
        }
*** Caught exception setting key "lat" : [<Location 0x85a20e0> setNilValueForKey]: could not set nil as the value for the key lat.

Is this intended functionality? Should it not just skip null values?

Infinite loop caused by -description and -isEqual:

Getting a weird issue here in an MTLModel subclass. I'm using it in conjunction with some ReactiveCocoa stuff (which may or may not be related to the issue). It goes into an infinite loop, here's the stack trace:

Screen Shot 2013-04-05 at 8 32 12 PM

I tried returning @"" from -description to see if that would resolve the issue. An infinite loop still occurred, this time in -isEqual::

Screen Shot 2013-04-05 at 8 32 48 PM

The infinite loop happens on this particular line:

Screen Shot 2013-04-05 at 8 35 12 PM

I'm not overriding -isEqual: or -description anywhere in the class hierarchy, so I'm not sure what would cause this. It's worth noting that the class this happens on has a property that is a weak reference to another MTLModel subclass that shares the same base superclass (which directly inherits from MTLModel). Basically looks like this:

@interface Item : MTLModel
@end

@interface Row : Item
@property (nonatomic, weak) Section *parent;
@end

@interface Section : Item
@property (nonatomic, strong) NSArray *children; // array of Row objects
@end

Implementation instructions

Sorry if this isn't the best place to do this, I wasn't aware of any Mantle mailing list...

Is there any good resource for additional details on actually implementing Mantle into a project? After completing the Getting stated (ie script/bootstrap) I'm assuming there are additional steps. Like building the framework, perhaps some dependencies. I'm honestly just not sure. This is probably obvious to most people, unfortunately I'm getting a litte hung up.

Thanks.

Question about an implementation detail - associated objects vs instance variables

I have a question about some of the code inside MTLModel. It's not really an "issue" as such, but it seems like this is the best place to ask anyway.

In +propertyKeys, the keys are cached once they have been calculated once, and stored using an associated object. What was the motivation behind using an associated object here? Is there a performance benefit over an instance variable or property that I'm not aware of (I'd be extremely surprised if so!), or is it simply a stylistic preference for some reason?

Thanks for your work on Mantle by the way. I implemented some new models using it yesterday and it's working well. I think it could do with a bit more documentation though - I'll try to find time to submit a PR this weekend 😉

MTLValueTransformer can't return NSUInteger, only a value object pointer

I'm trying to code a property similar the the GHIssueState used in your blog post:

+ (NSValueTransformer *)stateTransformer {
    NSDictionary *states = @{
        @"open": @(GHIssueStateOpen),
        @"closed": @(GHIssueStateClosed)
    };

    return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
        return [states[str] unsignedIntegerValue];
    } reverseBlock:^(GHIssueState state) {
        return [states allKeysForObject:@(state)].lastObject;
    }];
}

However, the code above gives me a compiler warning:

Incompatible block pointer types sending 'id (^)(GHIssueState)' to parameter of type 'MTLValueTransformerBlock' (aka 'id (^)(__strong id)')

It appears the NSValueTransformer requires an object, not a scalar, to be returned by the forward/reverse blocks. I was hoping to use an NSUInteger enum similar to your example, but I can't figure out how to make it work with transformers short of using an NSNumber instead. Is this a bug in the example or am I doing something wrong?

MTLModel - propertyKeys - not always freeing attributes

in MTLMode propertyKeys, you use ext_copyPropertyAttributes to check if the property is readonly and then ignore it.

    [self enumeratePropertiesUsingBlock:^(objc_property_t property, BOOL *stop) {
        ext_propertyAttributes *attributes = ext_copyPropertyAttributes(property);
        if (attributes->readonly && attributes->ivar == NULL) return;

        free(attributes);

        NSString *key = @(property_getName(property));
        [keys addObject:key];
    }];

I believe you should call free(attributes) before returning

[self enumeratePropertiesUsingBlock:^(objc_property_t property, BOOL *stop) {
        ext_propertyAttributes *attributes = ext_copyPropertyAttributes(property);
        if (attributes->readonly && attributes->ivar == NULL) {
                    free(attributes);
                    return;
                }

        free(attributes);

        NSString *key = @(property_getName(property));
        [keys addObject:key];
    }];

"config.h" not found (CocoaPods+iOS7)

Hi,

I'm working on iOS 7, using Xcode 5 (preview of course), and when I compile my project, I get this error:

Mantle/metamacros.h:13:10: fatal error: 'config.h' file not found

Using :head for the pod.

Any idea?

Thanks,
Jérémy

Attempt to insert nil value exception when making JSON tree flat

I am instantiating model objects using MTLJSONAdapter

MTLModel *parsedObject = [MTLJSONAdapter modelOfClass:Player.class fromJSONDictionary:json error:&error];

My model looks like:

@implementaiton Player
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return {
            @"ID": @"account_id",
            @"clanID": @"clan.clan_id"
            }];
}
@end

Parsed json is the following:

NSDictionary *json = @{@"clan": NSNull.null, @"account_id": @1};

Executing the mapping produces an exception:

NSInvalidArgumentException "[<Player_ 0xd1609f0> setNilValueForKey]: could not set nil as the value for the key clanID." raised

Is there any workaround for this? (The intent is to 'flatten' nested clan entity to player properties)

Mantle and mutable properties (specifically arrays)

I'm not sure if I'm doing something incorrectly, or if this is a limitation of Mantle.

Lets say I have a mutable array on my model, like so:

@property (nonatomic, strong) NSMutableArray *stuff;

When initializing from an external representation, Mantle decides it wants to use an immutable type for my array instead of using the mutable variant.

Any thoughts on this?

Convert null values to NSNull using the array transformer

Originally mentioned in #106.

When using mtl_JSONArrayTransformerWithModelClass:, this array:

[
    {"test_id":"test_id_1"}, 
    null, 
    {"test_id":"test_id_3"}, 
    {"test_id":"test_id_4"}
]

… should turn into an NSArray of [Model, NSNull, Model, Model]. Right now, it asserts because the null was unexpected.

Is it possible to update a NSManagedObject?

In MTLManagedObjectAdapter.h I see following class methods:

+modelOfClass:fromManagedObject:error:
+managedObjectFromModel:insertingIntoContext:error:

But is it possible to update a NSManagedObject using MTLModel+MTLManagedObjectSerializing+MTLManagedObjectAdapter magic?

More pre-defined NSValueTransformers?

Is anyone against adding a few more for integer, float, enum, and maybe even date?

I would also prefer if MTLBooleanValueTransformerName returns NO for bogus values.

Nevermind I was confused.

Skipping nil or absent values with mergeValuesForKeysFromModel:

I have a User object which has an accessToken property. I also have two API calls: one that gets the logged in user's info, including accessToken, and one that gets any user's profile, and excludes accessToken.

Sometimes I want to get the logged in user's public profile, so I get the return JSON dictionary from the profile API, and merge it with the logged in user. However, since this dictionary doesn't contain an accessToken, the user's current accessToken is set to nil upon merge.

Is there anyway during a merge that I can skip keys that are nil or absent?

Optional Attribues

i instanciate some mtlModels using modelWithDictionary
the problem here is that i have some attribues in my dictionary (which i get from a json string) that i don't want to store in my model, because they are not neccessary for my app but exist in my json (which i can't change, because of an external API).

is there a way to say mantle to skip attributes from a dictionary/json string?

thanks!

"unrecognized selector" because mapping don't work

i got two questions (marked bold)

i get a "unrecognized selector" exception for the following situation:

JSON

{
    "timezone": "America/Los_Angeles",
    "offset": -7,
    "currently": {
        "time": 1366791192,
        "summary": "some"
    },
    "daily": {
        "summary": "some",
        "icon": "stuff",
        "data": [
            {
                "time": 1366786800,
                "summary": "some"
            },
            {
                "time": 1366873200,
                "summary": "stuff"
            }
        ]
    }
}

and i have two models for this: Forecast and ForecastDataPoint.
Forecast should implement timezone, offset and one ForecastDataPoint (represented by currently in the JSON file and an array of dataPoints (represented by daily.data in the JSON file).

@interface MJUForecastModel : MTLModel

@property (nonatomic, copy, readonly) NSString *timezone;
@property (nonatomic, copy, readonly) NSNumber *offset;
@property (nonatomic, strong, readonly) MJUForecastDataPointModel *currentDataPoint;
@property (nonatomic, copy, readonly) NSArray *dailyDataPoints;

@end
@interface MJUForecastDataPointModel : MTLModel

@property (nonatomic, copy, readonly) NSNumber *time;
@property (nonatomic, copy, readonly) NSString *summary;

@end

so what i tried to map currently from the JSON to currentDataPoint in my MJUForecastModel Object is to implement

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{
             @"currentDataPoint": @"currently",
             };
}

but this method doesn't get called anyway (i added MTLJSONSerializing to my model)

what am i doing wrong here?

also my JSON Model implements a daily dict which i don't want to add directly to my model. what i want is to add the data array of the daily dict into dailyDataPoints of my model which should be MJUForecastDataPointModel Objects itself (and ignore summary and icon properties of the daily JSON)

is there a way to do this?

Invert a value transformer

We should offer an NSValueTransformer category method which reverses the direction of an existing transformer.

This will be especially useful for reusing transformers with the Core Data adapter. For example, you might do the following:

  • NSString to NSURL when parsing JSON into a MTLModel.
  • NSURL to NSString when saving the same MTLModel class into Core Data.

Rather than define two built-in transformers, the user can just flip the existing one for their needs.

Lightweight Vs. Heavyweight

Hey,

Would it be possible to remove the dependency on libextobjc (and therefore the dependency on both Configuration and libffi)?

For what is a relatively lightweight framework, including all these dependencies makes it much fatter. Since Mantle actually only depends on three .h/.m files from libextobjc and those themselves have no external dependencies, they could easily be rolled up into Mantle.

Please let me know your thoughts.

Support error reporting from -initWithDictionary:

This would probably require changing the method signature to -initWithDictionary:error:, but would give consumers a lot more flexibility to handle errors.

We could also report things like KVC validation errors this way, and MTLJSONAdapter could have error parameters added to it as well (as discussed in #64.)

Include version number in header

Would it be possible to include the version number in the header file comment for the library? It would make it easier to keep track of the version currently included in a project (assuming the project doesn't use CocoaPods, etc).

Crash on Release build

I'm getting a weird crash that only happens with release builds.

0   libsystem_kernel.dylib          0x383d1350 __pthread_kill + 8
1   libsystem_c.dylib               0x3391e11e pthread_kill + 54
2   libsystem_c.dylib               0x3395a9f2 <redacted> + 90
3   libsystem_c.dylib               0x3395b03e __stack_chk_fail + 194
4   TeaSound                        0x00084a96 ext_copyPropertyAttributes + 1158
5   TeaSound                        0x0007bb2e __24+[MTLModel propertyKeys]_block_invoke_0 + 10
6   TeaSound                        0x0007b9de +[MTLModel enumeratePropertiesUsingBlock:] + 346
7   TeaSound                        0x0007baf2 +[MTLModel propertyKeys] + 158
8   TeaSound                        0x0007b526 -[MTLModel initWithExternalRepresentation:] + 290

It appears to be related to this issue in libextobjc: jspahrsummers/libextobjc#8, as after I applied that change (added "+1" to ext_copyPropertyAttributes) the crash went away.

Again, no crashes on-device or simulator with debug builds, only release builds delivered ad-hoc.

Happy to provide any useful info regarding this (stuff about my model, external representation, etc.), but I'm assuming it's just time to pull some changes over from libextobjc? This issue was closed just 7 days ago.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.