{"id":15025326,"url":"https://github.com/mantle/mantle","last_synced_at":"2025-05-13T00:09:52.543Z","repository":{"id":4542794,"uuid":"5683364","full_name":"Mantle/Mantle","owner":"Mantle","description":"Model framework for Cocoa and Cocoa Touch","archived":false,"fork":false,"pushed_at":"2022-10-18T09:40:02.000Z","size":1635,"stargazers_count":11305,"open_issues_count":0,"forks_count":1483,"subscribers_count":391,"default_branch":"master","last_synced_at":"2025-05-13T00:09:45.870Z","etag":null,"topics":["boilerplate","json","objective-c"],"latest_commit_sha":null,"homepage":"","language":"Objective-C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Mantle.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":["jspahrsummers","robb"]}},"created_at":"2012-09-05T06:24:59.000Z","updated_at":"2025-05-06T00:50:12.000Z","dependencies_parsed_at":"2022-07-12T15:01:10.255Z","dependency_job_id":null,"html_url":"https://github.com/Mantle/Mantle","commit_stats":null,"previous_names":[],"tags_count":45,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mantle%2FMantle","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mantle%2FMantle/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mantle%2FMantle/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mantle%2FMantle/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Mantle","download_url":"https://codeload.github.com/Mantle/Mantle/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253843215,"owners_count":21972873,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["boilerplate","json","objective-c"],"created_at":"2024-09-24T20:02:05.170Z","updated_at":"2025-05-13T00:09:52.519Z","avatar_url":"https://github.com/Mantle.png","language":"Objective-C","readme":"# Mantle\n\n[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)\n[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Mantle.svg)](https://img.shields.io/cocoapods/v/Mantle.svg)\n[![SPM compatible](https://img.shields.io/badge/SPM-compatible-4BC51D.svg?style=flat)](https://swift.org/package-manager)\n\nMantle makes it easy to write a simple model layer for your Cocoa or Cocoa Touch application.\n\n## The Typical Model Object\n\nWhat's wrong with the way model objects are usually written in Objective-C?\n\nLet's use the [GitHub API](http://developer.github.com) for demonstration. How\nwould one typically represent a [GitHub\nissue](http://developer.github.com/v3/issues/#get-a-single-issue) in\nObjective-C?\n\n```objc\ntypedef enum : NSUInteger {\n    GHIssueStateOpen,\n    GHIssueStateClosed\n} GHIssueState;\n\n@interface GHIssue : NSObject \u003cNSCoding, NSCopying\u003e\n\n@property (nonatomic, copy, readonly) NSURL *URL;\n@property (nonatomic, copy, readonly) NSURL *HTMLURL;\n@property (nonatomic, copy, readonly) NSNumber *number;\n@property (nonatomic, assign, readonly) GHIssueState state;\n@property (nonatomic, copy, readonly) NSString *reporterLogin;\n@property (nonatomic, copy, readonly) NSDate *updatedAt;\n@property (nonatomic, strong, readonly) GHUser *assignee;\n@property (nonatomic, copy, readonly) NSDate *retrievedAt;\n\n@property (nonatomic, copy) NSString *title;\n@property (nonatomic, copy) NSString *body;\n\n- (id)initWithDictionary:(NSDictionary *)dictionary;\n\n@end\n```\n\n```objc\n@implementation GHIssue\n\n+ (NSDateFormatter *)dateFormatter {\n    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];\n    dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@\"en_US_POSIX\"];\n    dateFormatter.dateFormat = @\"yyyy-MM-dd'T'HH:mm:ss'Z'\";\n    return dateFormatter;\n}\n\n- (id)initWithDictionary:(NSDictionary *)dictionary {\n    self = [self init];\n    if (self == nil) return nil;\n\n    _URL = [NSURL URLWithString:dictionary[@\"url\"]];\n    _HTMLURL = [NSURL URLWithString:dictionary[@\"html_url\"]];\n    _number = dictionary[@\"number\"];\n\n    if ([dictionary[@\"state\"] isEqualToString:@\"open\"]) {\n        _state = GHIssueStateOpen;\n    } else if ([dictionary[@\"state\"] isEqualToString:@\"closed\"]) {\n        _state = GHIssueStateClosed;\n    }\n\n    _title = [dictionary[@\"title\"] copy];\n    _retrievedAt = [NSDate date];\n    _body = [dictionary[@\"body\"] copy];\n    _reporterLogin = [dictionary[@\"user\"][@\"login\"] copy];\n    _assignee = [[GHUser alloc] initWithDictionary:dictionary[@\"assignee\"]];\n\n    _updatedAt = [self.class.dateFormatter dateFromString:dictionary[@\"updated_at\"]];\n\n    return self;\n}\n\n- (id)initWithCoder:(NSCoder *)coder {\n    self = [self init];\n    if (self == nil) return nil;\n\n    _URL = [coder decodeObjectForKey:@\"URL\"];\n    _HTMLURL = [coder decodeObjectForKey:@\"HTMLURL\"];\n    _number = [coder decodeObjectForKey:@\"number\"];\n    _state = [coder decodeUnsignedIntegerForKey:@\"state\"];\n    _title = [coder decodeObjectForKey:@\"title\"];\n    _retrievedAt = [NSDate date];\n    _body = [coder decodeObjectForKey:@\"body\"];\n    _reporterLogin = [coder decodeObjectForKey:@\"reporterLogin\"];\n    _assignee = [coder decodeObjectForKey:@\"assignee\"];\n    _updatedAt = [coder decodeObjectForKey:@\"updatedAt\"];\n\n    return self;\n}\n\n- (void)encodeWithCoder:(NSCoder *)coder {\n    if (self.URL != nil) [coder encodeObject:self.URL forKey:@\"URL\"];\n    if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURL forKey:@\"HTMLURL\"];\n    if (self.number != nil) [coder encodeObject:self.number forKey:@\"number\"];\n    if (self.title != nil) [coder encodeObject:self.title forKey:@\"title\"];\n    if (self.body != nil) [coder encodeObject:self.body forKey:@\"body\"];\n    if (self.reporterLogin != nil) [coder encodeObject:self.reporterLogin forKey:@\"reporterLogin\"];\n    if (self.assignee != nil) [coder encodeObject:self.assignee forKey:@\"assignee\"];\n    if (self.updatedAt != nil) [coder encodeObject:self.updatedAt forKey:@\"updatedAt\"];\n\n    [coder encodeUnsignedInteger:self.state forKey:@\"state\"];\n}\n\n- (id)copyWithZone:(NSZone *)zone {\n    GHIssue *issue = [[self.class allocWithZone:zone] init];\n    issue-\u003e_URL = self.URL;\n    issue-\u003e_HTMLURL = self.HTMLURL;\n    issue-\u003e_number = self.number;\n    issue-\u003e_state = self.state;\n    issue-\u003e_reporterLogin = self.reporterLogin;\n    issue-\u003e_assignee = self.assignee;\n    issue-\u003e_updatedAt = self.updatedAt;\n\n    issue.title = self.title;\n    issue-\u003e_retrievedAt = [NSDate date];\n    issue.body = self.body;\n\n    return issue;\n}\n\n- (NSUInteger)hash {\n    return self.number.hash;\n}\n\n- (BOOL)isEqual:(GHIssue *)issue {\n    if (![issue isKindOfClass:GHIssue.class]) return NO;\n\n    return [self.number isEqual:issue.number] \u0026\u0026 [self.title isEqual:issue.title] \u0026\u0026 [self.body isEqual:issue.body];\n}\n\n@end\n```\n\nWhew, that's a lot of boilerplate for something so simple! And, even then, there\nare some problems that this example doesn't address:\n\n * There's no way to update a `GHIssue` with new data from the server.\n * There's no way to turn a `GHIssue` _back_ into JSON.\n * `GHIssueState` shouldn't be encoded as-is. If the enum changes in the future,\n   existing archives might break.\n * If the interface of `GHIssue` changes down the road, existing archives might\n   break.\n\n## Why Not Use Core Data?\n\nCore Data solves certain problems very well. If you need to execute complex\nqueries across your data, handle a huge object graph with lots of relationships,\nor support undo and redo, Core Data is an excellent fit.\n\nIt does, however, come with a couple of pain points:\n\n * **There's still a lot of boilerplate.** Managed objects reduce some of the\n   boilerplate seen above, but Core Data has plenty of its own. Correctly\n   setting up a Core Data stack (with a persistent store and persistent store\n   coordinator) and executing fetches can take many lines of code.\n * **It's hard to get right.** Even experienced developers can make mistakes\n   when using Core Data, and the framework is not forgiving.\n\nIf you're just trying to access some JSON objects, Core Data can be a lot of\nwork for little gain.\n\nNonetheless, if you're using or want to use Core Data in your app already,\nMantle can still be a convenient translation layer between the API and your\nmanaged model objects.\n\n## MTLModel\n\nEnter\n**[MTLModel](https://github.com/github/Mantle/blob/master/Mantle/MTLModel.h)**.\nThis is what `GHIssue` looks like inheriting from `MTLModel`:\n\n```objc\ntypedef enum : NSUInteger {\n    GHIssueStateOpen,\n    GHIssueStateClosed\n} GHIssueState;\n\n@interface GHIssue : MTLModel \u003cMTLJSONSerializing\u003e\n\n@property (nonatomic, copy, readonly) NSURL *URL;\n@property (nonatomic, copy, readonly) NSURL *HTMLURL;\n@property (nonatomic, copy, readonly) NSNumber *number;\n@property (nonatomic, assign, readonly) GHIssueState state;\n@property (nonatomic, copy, readonly) NSString *reporterLogin;\n@property (nonatomic, strong, readonly) GHUser *assignee;\n@property (nonatomic, copy, readonly) NSDate *updatedAt;\n\n@property (nonatomic, copy) NSString *title;\n@property (nonatomic, copy) NSString *body;\n\n@property (nonatomic, copy, readonly) NSDate *retrievedAt;\n\n@end\n```\n\n```objc\n@implementation GHIssue\n\n+ (NSDateFormatter *)dateFormatter {\n    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];\n    dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@\"en_US_POSIX\"];\n    dateFormatter.dateFormat = @\"yyyy-MM-dd'T'HH:mm:ss'Z'\";\n    return dateFormatter;\n}\n\n+ (NSDictionary *)JSONKeyPathsByPropertyKey {\n    return @{\n        @\"URL\": @\"url\",\n        @\"HTMLURL\": @\"html_url\",\n        @\"number\": @\"number\",\n        @\"state\": @\"state\",\n        @\"reporterLogin\": @\"user.login\",\n        @\"assignee\": @\"assignee\",\n        @\"updatedAt\": @\"updated_at\"\n    };\n}\n\n+ (NSValueTransformer *)URLJSONTransformer {\n    return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];\n}\n\n+ (NSValueTransformer *)HTMLURLJSONTransformer {\n    return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];\n}\n\n+ (NSValueTransformer *)stateJSONTransformer {\n    return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{\n        @\"open\": @(GHIssueStateOpen),\n        @\"closed\": @(GHIssueStateClosed)\n    }];\n}\n\n+ (NSValueTransformer *)assigneeJSONTransformer {\n    return [MTLJSONAdapter dictionaryTransformerWithModelClass:GHUser.class];\n}\n\n+ (NSValueTransformer *)updatedAtJSONTransformer {\n    return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {\n        return [self.dateFormatter dateFromString:dateString];\n    } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {\n        return [self.dateFormatter stringFromDate:date];\n    }];\n}\n\n- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {\n    self = [super initWithDictionary:dictionaryValue error:error];\n    if (self == nil) return nil;\n\n    // Store a value that needs to be determined locally upon initialization.\n    _retrievedAt = [NSDate date];\n\n    return self;\n}\n\n@end\n```\n\nNotably absent from this version are implementations of `\u003cNSCoding\u003e`,\n`\u003cNSCopying\u003e`, `-isEqual:`, and `-hash`. By inspecting the `@property`\ndeclarations you have in your subclass, `MTLModel` can provide default\nimplementations for all these methods.\n\nThe problems with the original example all happen to be fixed as well:\n\n\u003e There's no way to update a `GHIssue` with new data from the server.\n\n`MTLModel` has an extensible `-mergeValuesForKeysFromModel:` method, which makes\nit easy to specify how new model data should be integrated.\n\n\u003e There's no way to turn a `GHIssue` _back_ into JSON.\n\nThis is where reversible transformers really come in handy. `+[MTLJSONAdapter\nJSONDictionaryFromModel:error:]` can transform any model object conforming to\n`\u003cMTLJSONSerializing\u003e` back into a JSON dictionary. `+[MTLJSONAdapter\nJSONArrayFromModels:error:]` is the same but turns an array of model objects into an JSON array of dictionaries.\n\n\u003e If the interface of `GHIssue` changes down the road, existing archives might break.\n\n`MTLModel` automatically saves the version of the model object that was used for\narchival. When unarchiving, `-decodeValueForKey:withCoder:modelVersion:` will\nbe invoked if overridden, giving you a convenient hook to upgrade old data.\n\n## MTLJSONSerializing\n\nIn order to serialize your model objects from or into JSON, you need to\nimplement `\u003cMTLJSONSerializing\u003e` in your `MTLModel` subclass. This allows you to\nuse `MTLJSONAdapter` to convert your model objects from JSON and back:\n\n```objc\nNSError *error = nil;\nXYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:\u0026error];\n```\n\n```objc\nNSError *error = nil;\nNSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:user error:\u0026error];\n```\n\n### `+JSONKeyPathsByPropertyKey`\n\nThe dictionary returned by this method specifies how your model object's\nproperties map to the keys in the JSON representation, for example:\n\n```objc\n\n@interface XYUser : MTLModel\n\n@property (readonly, nonatomic, copy) NSString *name;\n@property (readonly, nonatomic, strong) NSDate *createdAt;\n\n@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;\n@property (readonly, nonatomic, strong) XYHelper *helper;\n\n@end\n\n@implementation XYUser\n\n+ (NSDictionary *)JSONKeyPathsByPropertyKey {\n    return @{\n        @\"name\": @\"name\",\n        @\"createdAt\": @\"created_at\"\n    };\n}\n\n- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {\n    self = [super initWithDictionary:dictionaryValue error:error];\n    if (self == nil) return nil;\n\n    _helper = [XYHelper helperWithName:self.name createdAt:self.createdAt];\n\n    return self;\n}\n\n@end\n```\n\nIn this example, the `XYUser` class declares four properties that Mantle\nhandles in different ways:\n\n- `name` is mapped to a key of the same name in the JSON representation.\n- `createdAt` is converted to its snake case equivalent.\n- `meUser` is not serialized into JSON.\n- `helper` is initialized exactly once after JSON deserialization.\n\nUse `-[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]` if your\nmodel's superclass also implements `MTLJSONSerializing` to merge their mappings.\n\nIf you'd like to map all properties of a Model class to themselves, you can use\nthe `+[NSDictionary mtl_identityPropertyMapWithModel:]` helper method.\n\nWhen deserializing JSON using\n`+[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]`, JSON keys that don't\ncorrespond to a property name or have an explicit mapping are ignored:\n\n```objc\nNSDictionary *JSONDictionary = @{\n    @\"name\": @\"john\",\n    @\"created_at\": @\"2013/07/02 16:40:00 +0000\",\n    @\"plan\": @\"lite\"\n};\n\nXYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:\u0026error];\n```\n\nHere, the `plan` would be ignored since it neither matches a property name of\n`XYUser` nor is it otherwise mapped in `+JSONKeyPathsByPropertyKey`.\n\n### `+JSONTransformerForKey:`\n\nImplement this optional method to convert a property from a different type when\ndeserializing from JSON.\n\n```\n+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key {\n    if ([key isEqualToString:@\"createdAt\"]) {\n        return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName];\n    }\n\n    return nil;\n}\n```\n\n`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`.\n\nFor added convenience, if you implement `+\u003ckey\u003eJSONTransformer`,\n`MTLJSONAdapter` will use the result of that method instead. For example, dates\nthat are commonly represented as strings in JSON can be transformed to `NSDate`s\nlike so:\n\n```objc\n    return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {\n        return [self.dateFormatter dateFromString:dateString];\n    } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {\n        return [self.dateFormatter stringFromDate:date];\n    }];\n}\n```\n\nIf the transformer is reversible, it will also be used when serializing the\nobject into JSON.\n\n### `+classForParsingJSONDictionary:`\n\nIf you are implementing a class cluster, implement this optional method to\ndetermine which subclass of your base class should be used when deserializing an\nobject from JSON.\n\n```objc\n@interface XYMessage : MTLModel\n\n@end\n\n@interface XYTextMessage: XYMessage\n\n@property (readonly, nonatomic, copy) NSString *body;\n\n@end\n\n@interface XYPictureMessage : XYMessage\n\n@property (readonly, nonatomic, strong) NSURL *imageURL;\n\n@end\n\n@implementation XYMessage\n\n+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {\n    if (JSONDictionary[@\"image_url\"] != nil) {\n        return XYPictureMessage.class;\n    }\n\n    if (JSONDictionary[@\"body\"] != nil) {\n        return XYTextMessage.class;\n    }\n\n    NSAssert(NO, @\"No matching class for the JSON dictionary '%@'.\", JSONDictionary);\n    return self;\n}\n\n@end\n```\n\n`MTLJSONAdapter` will then pick the class based on the JSON dictionary you pass\nin:\n\n```objc\nNSDictionary *textMessage = @{\n    @\"id\": @1,\n    @\"body\": @\"Hello World!\"\n};\n\nNSDictionary *pictureMessage = @{\n    @\"id\": @2,\n    @\"image_url\": @\"http://example.com/lolcat.gif\"\n};\n\nXYTextMessage *messageA = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:textMessage error:NULL];\n\nXYPictureMessage *messageB = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:pictureMessage error:NULL];\n```\n\n## Persistence\n\nMantle doesn't automatically persist your objects for you. However, `MTLModel`\ndoes conform to `\u003cNSCoding\u003e`, so model objects can be archived to disk using\n`NSKeyedArchiver`.\n\nIf you need something more powerful, or want to avoid keeping your whole model\nin memory at once, Core Data may be a better choice.\n\n## System Requirements\n\nMantle supports the following platform deployment targets:\n\n* macOS 10.10+\n* iOS 9.0+\n* tvOS 9.0+\n* watchOS 2.0+\n\n## Importing Mantle\n\n### Manually\n\nTo add Mantle to your application:\n\n 1. Add the Mantle repository as a submodule of your application's repository.\n 1. Run `git submodule update --init --recursive` from within the Mantle folder.\n 1. Drag and drop `Mantle.xcodeproj` into your application's Xcode project.\n 1. On the \"General\" tab of your application target, add `Mantle.framework` to the \"Embedded Binaries\".\n\nIf you’re instead developing Mantle on its own, use the `Mantle.xcworkspace` file.\n\n### [Carthage](https://github.com/Carthage/Carthage)\n\nSimply add Mantle to your `Cartfile`:\n\n```\ngithub \"Mantle/Mantle\"\n```\n\n### [CocoaPods](https://cocoapods.org/pods/Mantle)\n\nAdd Mantle to your `Podfile` under the build target they want it used in:\n\n```\ntarget 'MyAppOrFramework' do\n  pod 'Mantle'\nend\n```\n\nThen run a `pod install` within Terminal or the [CocoaPods app](https://cocoapods.org/app).\n\n### [Swift Package Manager](https://swift.org/package-manager)\n\nIf you are writing an application, add Mantle to your project dependencies [directly within Xcode](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app).\n\nIf you are writing a package that requires Mantle as dependency, add it to the `dependencies` list in its `Package.swift` manifest, for example:\n\n```\ndependencies: [\n    .package(url: \"https://github.com/Mantle/Mantle.git\", .upToNextMajor(from: \"2.0.0\"))\n]\n```\n\n## License\n\nMantle is released under the MIT license. See\n[LICENSE.md](https://github.com/github/Mantle/blob/master/LICENSE.md).\n\n## More Info\n\nHave a question? Please [open an issue](https://github.com/Mantle/Mantle/issues/new)!\n","funding_links":["https://github.com/sponsors/jspahrsummers","https://github.com/sponsors/robb"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmantle%2Fmantle","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmantle%2Fmantle","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmantle%2Fmantle/lists"}