【Objective-C】MantleでJSONをマッピング

久しぶりにObjective-Cで開発をする機会をいただいたので、
今更ですがマッピングライブラリである[Mantle](https://github.com/Mantle/Mantle)の使い方をまとめたいと思います。

# Mantle

Objective-C向けのマッピングのフレームワークです。
類似のフレームワークと大差をつけるスター数を獲得しています。

# Sample

[サンプルプロジェクト](https://github.com/KentaKudo/MantleExample.git)

今回はMantleのREADMEと同様、[GitHub IssueのAPI](https://developer.github.com/v3/issues/#get-a-single-issue)を使用します。

```
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オブジェクトを作成します。

```objc:GHIssue.h
#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

```

```objc:GHUser.h
#import <Foundation/Foundation.h>
#import <Mantle/Mantle.h>

@interface GHUser : MTLModel<MTLJSONSerializing>

@property (nonatomic, readonly) BOOL siteAdmin;

@end

```

```objc:GHLabel.h
#import <Foundation/Foundation.h>
#import <Mantle/Mantle.h>

@interface GHLabel : MTLModel<MTLJSONSerializing>

@property (nonatomic, readonly) NSString *name;

@end

```

GHClosedByクラスはあえてNSObjectのまま定義しています。

```objc:GHClosedBy.h
#import <Foundation/Foundation.h>

@interface GHClosedBy : NSObject

@property (nonatomic) NSString *login;

@end

```

# APIキーと変数名の対応の登録

JSONKeyPathsByPropertyKeyメソッドにAPIキーと変数名の対応を定義します。
NSDictionaryのキーに変数名、値にAPIキーを設定します。

```objc:GHIssue.m
#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

```

```objc:GHUser.m
#import "GHUser.h"

@implementation GHUser

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

@end

```

```objc:GHLabel.m
#import "GHLabel.h"

@implementation GHLabel

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

@end

```

# 数値、文字列、ブール値

なにもしなくてOK✌︎

# 手動でのマッピング

**+ (NSValueTransfer *)○○JSONTransfer**メソッドを使用します(○○には変数名)。

## NSURL
NSValueTransformerクラスの**valueTransformerForName:**を使用する。

例)サンプルのGHIssueクラスのurlのマッピング

```objc:GHIssue.m
#import "GHIssue.h"

@implementation GHIssue

...

+ (NSValueTransformer *)urlJSONTransformer {
    return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}

...

@end

```

## enum

NSValueTransformerクラスの**mtl_valueMappingTransformerWithDictionary:**を使用します。

例)サンプルのGHIssueクラスのstateのマッピング

```objc:GHIssue.m
#import "GHIssue.h"

@implementation GHIssue

...

+ (NSValueTransformer *)stateJSONTransformer {
    return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
                                                                           @"open"  : @(GHIssueStateOpen),
                                                                           @"closed": @(GHIssueStateClosed)
                                                                           }];
}

...

@end

```

## NSDate
MTLValueTransformerクラスの**transformerUsingForwardBlock:reverseBlock:**を使用します。

例)サンプルのGHIssueクラスのcreatedAtのマッピング

```objc:GHIssue.m
#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に使用されるメソッド)

```objc:GHIssue.m
#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

```objc:GHIssue.m
#import "GHIssue.h"

@implementation GHIssue

...

+ (NSValueTransformer *)labelsJSONTransformer {
    return [MTLJSONAdapter arrayTransformerWithModelClass:GHLabel.class];
}

...

@end

```

# 取得

MTLJSONAdapterクラスの**modelOfClass:fromJSONDictionary:error:**を使用します。

```objc
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
```

無事取得できているようです。

# おわりに

想定したとおりにマッピングされるので、
学習コストが低いのが一番の良さだと感じました。

# 参考資料

- [モデルクラスとNSDictionaryとの相互変換を楽に行えるようにするライブラリMantleを試す](http://safx-dev.blogspot.jp/2013/12/nsdictionarymantle.html)
- [Simplifying RESTful API Use and Data Persistence on iOS with Mantle and Realm](https://www.toptal.com/ios/simplifying-restful-api-use-and-data-persistence-on-ios-with-mantle-and-realm)
- [Mantle a Model Framework for Objective-C](https://github.com/blog/1299-mantle-a-model-framework-for-objective-c)