Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

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

GHUser.h
#import <Foundation/Foundation.h>
#import <Mantle/Mantle.h>

@interface GHUser : MTLModel<MTLJSONSerializing>

@property (nonatomic, readonly) BOOL siteAdmin;

@end

GHLabel.h
#import <Foundation/Foundation.h>
#import <Mantle/Mantle.h>

@interface GHLabel : MTLModel<MTLJSONSerializing>

@property (nonatomic, readonly) NSString *name;

@end

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

GHClosedBy.h
#import <Foundation/Foundation.h>

@interface GHClosedBy : NSObject

@property (nonatomic) NSString *login;

@end

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

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

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

GHUser.m
#import "GHUser.h"

@implementation GHUser

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

@end

GHLabel.m
#import "GHLabel.h"

@implementation GHLabel

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

@end

数値、文字列、ブール値

なにもしなくてOK✌︎

手動でのマッピング

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

NSURL

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

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

GHIssue.m
#import "GHIssue.h"

@implementation GHIssue

...

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

...

@end

enum

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

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

GHIssue.m
#import "GHIssue.h"

@implementation GHIssue

...

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

...

@end

NSDate

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

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

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に使用されるメソッド)

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

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

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

おわりに

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

参考資料

KentaKudo
イギリスの会社でソフトウェアエンジニアをしています。
https://kentakudo.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away