久しぶりにObjective-Cで開発をする機会をいただいたので、
今更ですがマッピングライブラリであるMantleの使い方をまとめたいと思います。
Mantle
Objective-C向けのマッピングのフレームワークです。
類似のフレームワークと大差をつけるスター数を獲得しています。
Sample
今回はMantleのREADMEと同様、GitHub IssueのAPIを使用します。
https://api.github.com/repos/Mantle/Mantle/issues/1
レスポンスは以下の通りです。
{
"url": "https://api.github.com/repos/Mantle/Mantle/issues/1",
"repository_url": "https://api.github.com/repos/Mantle/Mantle",
"labels_url": "https://api.github.com/repos/Mantle/Mantle/issues/1/labels{/name}",
"comments_url": "https://api.github.com/repos/Mantle/Mantle/issues/1/comments",
"events_url": "https://api.github.com/repos/Mantle/Mantle/issues/1/events",
"html_url": "https://github.com/Mantle/Mantle/issues/1",
"id": 6798304,
"number": 1,
"title": "Add a protocol to manipulate all collections as sequences",
"user": {
"login": "jspahrsummers",
"id": 432536,
"avatar_url": "https://avatars.githubusercontent.com/u/432536?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/jspahrsummers",
"html_url": "https://github.com/jspahrsummers",
"followers_url": "https://api.github.com/users/jspahrsummers/followers",
"following_url": "https://api.github.com/users/jspahrsummers/following{/other_user}",
"gists_url": "https://api.github.com/users/jspahrsummers/gists{/gist_id}",
"starred_url": "https://api.github.com/users/jspahrsummers/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/jspahrsummers/subscriptions",
"organizations_url": "https://api.github.com/users/jspahrsummers/orgs",
"repos_url": "https://api.github.com/users/jspahrsummers/repos",
"events_url": "https://api.github.com/users/jspahrsummers/events{/privacy}",
"received_events_url": "https://api.github.com/users/jspahrsummers/received_events",
"type": "User",
"site_admin": false
},
"labels": [
{
"url": "https://api.github.com/repos/Mantle/Mantle/labels/enhancement",
"name": "enhancement",
"color": "84b6eb"
}
],
"state": "closed",
"locked": false,
"assignee": null,
"milestone": null,
"comments": 2,
"created_at": "2012-09-11T18:48:05Z",
"updated_at": "2012-10-31T23:44:30Z",
"closed_at": "2012-10-31T23:44:28Z",
"body": "It'd be useful to have something like a `<MTLSequence>` protocol, to which `NSArray`, `NSDictionary`, `NSOrderedSet`, and `NSSet` could conform, which would allow mapping, filtering, etc. in a generic way (a la Clojure).\r\n\r\nSuggested by @joshaber.",
"closed_by": {
"login": "jspahrsummers",
"id": 432536,
"avatar_url": "https://avatars.githubusercontent.com/u/432536?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/jspahrsummers",
"html_url": "https://github.com/jspahrsummers",
"followers_url": "https://api.github.com/users/jspahrsummers/followers",
"following_url": "https://api.github.com/users/jspahrsummers/following{/other_user}",
"gists_url": "https://api.github.com/users/jspahrsummers/gists{/gist_id}",
"starred_url": "https://api.github.com/users/jspahrsummers/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/jspahrsummers/subscriptions",
"organizations_url": "https://api.github.com/users/jspahrsummers/orgs",
"repos_url": "https://api.github.com/users/jspahrsummers/repos",
"events_url": "https://api.github.com/users/jspahrsummers/events{/privacy}",
"received_events_url": "https://api.github.com/users/jspahrsummers/received_events",
"type": "User",
"site_admin": false
}
}
MTLModelオブジェクトの作成
以下のようにMTLModelオブジェクトを作成します。
#import <Foundation/Foundation.h>
#import <Mantle/Mantle.h>
#import "GHUser.h"
#import "GHLabel.h"
#import "GHClosedBy.h"
typedef NS_ENUM(NSUInteger, GHIssueState) {
GHIssueStateOpen = 0,
GHIssueStateClosed
};
@interface GHIssue : MTLModel<MTLJSONSerializing>
@property (nonatomic, readonly) NSURL *url;
@property (nonatomic, readonly) NSInteger number;
@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) GHUser *user;
@property (nonatomic, readonly) NSArray<GHLabel *> *labels;
@property (nonatomic, readonly) NSDate *createdAt;
@property (nonatomic, readonly) GHIssueState state;
@property (nonatomic, readonly) GHClosedBy *closedBy;
@end
#import <Foundation/Foundation.h>
#import <Mantle/Mantle.h>
@interface GHUser : MTLModel<MTLJSONSerializing>
@property (nonatomic, readonly) BOOL siteAdmin;
@end
#import <Foundation/Foundation.h>
#import <Mantle/Mantle.h>
@interface GHLabel : MTLModel<MTLJSONSerializing>
@property (nonatomic, readonly) NSString *name;
@end
GHClosedByクラスはあえてNSObjectのまま定義しています。
#import <Foundation/Foundation.h>
@interface GHClosedBy : NSObject
@property (nonatomic) NSString *login;
@end
APIキーと変数名の対応の登録
JSONKeyPathsByPropertyKeyメソッドにAPIキーと変数名の対応を定義します。
NSDictionaryのキーに変数名、値にAPIキーを設定します。
#import "GHIssue.h"
@implementation GHIssue
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"url" : @"url",
@"number" : @"number",
@"title" : @"title",
@"user" : @"user",
@"labels" : @"labels",
@"createdAt": @"created_at",
@"state" : @"state",
@"closedBy" : @"closed_by"
};
}
@end
#import "GHUser.h"
@implementation GHUser
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"siteAdmin": @"site_admin"
};
}
@end
#import "GHLabel.h"
@implementation GHLabel
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"name": @"name"
};
}
@end
数値、文字列、ブール値
なにもしなくてOK✌︎
手動でのマッピング
+ (NSValueTransfer *)○○JSONTransferメソッドを使用します(○○には変数名)。
NSURL
NSValueTransformerクラスの**valueTransformerForName:**を使用する。
例)サンプルのGHIssueクラスのurlのマッピング
#import "GHIssue.h"
@implementation GHIssue
...
+ (NSValueTransformer *)urlJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
...
@end
enum
NSValueTransformerクラスの**mtl_valueMappingTransformerWithDictionary:**を使用します。
例)サンプルのGHIssueクラスのstateのマッピング
#import "GHIssue.h"
@implementation GHIssue
...
+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
@"open" : @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}
...
@end
NSDate
MTLValueTransformerクラスの**transformerUsingForwardBlock:reverseBlock:**を使用します。
例)サンプルのGHIssueクラスのcreatedAtのマッピング
#import "GHIssue.h"
@implementation GHIssue
...
+ (NSValueTransformer *)createdAtJSONTransformer {
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];
}];
}
//-----------------------------------------------------------------------
// Helper Method
+ (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;
}
...
@end
自分で定義したクラス
NSDate同様にMTLValueTransformerクラスの**transformerUsingForwardBlock:reverseBlock:**を使用します。
例)サンプルのGHClosedByクラスのマッピング
変数名はclosedBy
reverseBlock:を省略したメソッドを使いました。
(※reverseBlockはModel→NSDictionaryに使用されるメソッド)
#import "GHIssue.h"
@implementation GHIssue
...
+ (NSValueTransformer *)closedByJSONTransformer {
return [MTLValueTransformer transformerUsingForwardBlock:^id(NSDictionary *value, BOOL *success, NSError *__autoreleasing *error) {
GHClosedBy *closedBy = [GHClosedBy new];
closedBy.login = value[@"login"];
return closedBy;
}];
}
...
@end
Mantle Object in Mantle Object
単体の場合
なにもしなくてOK✌︎
例)サンプルのGHUserクラス
配列の場合
MTLJSONAdopterクラスの**arrayTransformerWithModelClass:**を使用します。
例)サンプルのGHLabelクラスの配列のマッピング
変数名はlabels
#import "GHIssue.h"
@implementation GHIssue
...
+ (NSValueTransformer *)labelsJSONTransformer {
return [MTLJSONAdapter arrayTransformerWithModelClass:GHLabel.class];
}
...
@end
取得
MTLJSONAdapterクラスの**modelOfClass:fromJSONDictionary:error:**を使用します。
NSURL *requestURL = [NSURL URLWithString:@"https://api.github.com/repos/Mantle/Mantle/issues/1"];
[[[NSURLSession sharedSession] dataTaskWithURL:requestURL
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSDictionary *jsonDic = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
GHIssue *issue = [MTLJSONAdapter modelOfClass:GHIssue.class
fromJSONDictionary:jsonDic
error:nil];
NSLog(@"issue.url = %@", issue.url);
NSLog(@"issue.number = %ld", issue.number);
NSLog(@"issue.title = %@", issue.title);
NSLog(@"issue.user.siteAdimin = %d", issue.user.siteAdmin);
NSLog(@"issue.labels[0].name = %@", issue.labels[0].name);
NSLog(@"issue.createdAt = %@", issue.createdAt);
NSLog(@"issue.state = %ld", issue.state);
NSLog(@"issue.closedBy.login = %@", issue.closedBy.login);
}] resume];
結果
2016-06-05 19:51:57.648 MantleExample[16262:864176] issue.url = https://api.github.com/repos/Mantle/Mantle/issues/1
2016-06-05 19:51:57.649 MantleExample[16262:864176] issue.number = 1
2016-06-05 19:51:57.649 MantleExample[16262:864176] issue.title = Add a protocol to manipulate all collections as sequences
2016-06-05 19:51:57.649 MantleExample[16262:864176] issue.user.siteAdimin = 0
2016-06-05 19:51:57.650 MantleExample[16262:864176] issue.labels[0].name = enhancement
2016-06-05 19:51:57.651 MantleExample[16262:864176] issue.createdAt = 2012-09-11 09:48:05 +0000
2016-06-05 19:51:57.652 MantleExample[16262:864176] issue.state = 1
2016-06-05 19:51:57.652 MantleExample[16262:864176] issue.closedBy.login = jspahrsummers
無事取得できているようです。
おわりに
想定したとおりにマッピングされるので、
学習コストが低いのが一番の良さだと感じました。