注:ここで作っていたライブラリですが、整備してライブラリとして公開しています。詳しくは
サーバの JSON テキストから Objective-C のインスタンスを生成するライブラリ、SimpleRemoteObject を公開しました。 を御覧ください。
======以下本文======
NSRails が感動的に使いやすくて素晴らしいのですが、今参加しているハッカソンで作っているアプリは、サーバ側が Rails じゃないのて使えなくて残念。というわけで、サーバからJSONのデータを取ってきて、独自クラスの配列にして返却するという仕組みを NSRails を若干参考にしつつ自分で実装してみました。
作ったクラスは以下の2つです。
MMRemoteClass - JSON の中で表現されているクラスをインスタンス化する為の親クラス
MMRemoteConfig - サーバの基本設定を保存しておくクラス
使い方
例えばURL
http://example.net/api/tag
にアクセスすると、以下のようなJSONが帰ってくるとします。
{
"meta": {
"limit": 20,
"next": null,
"offset": 0,
"previous": null,
"total_count": 3
},
"objects": [
{
"id": 2,
"name": "カフェ",
"resource_uri": "/api/tag/%E3%82%AB%E3%83%95%E3%82%A7",
"slug": "カフェ"
},
{
"id": 1,
"name": "ハッカソン",
"resource_uri": "/api/tag/%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3",
"slug": "ハッカソン"
},
{
"id": 3,
"name": "破滅",
"resource_uri": "/api/tag/%E7%A0%B4%E6%BB%85",
"slug": "破滅"
}
]
}
この JSON の "objects" キー内を、 name, resource_uri, slug というプロパティを持つ MMTag というクラスの配列に変換したい場合などに使えます。
まず、MMRemoteClass を拡張した MMTag クラスを作り、JSON内のプロパティ名と同じ物をプロパティ定義します。
//
// MMTag.h
// MoyaMap
//
// Created by Haruyuki Seki on 2/16/13.
// Copyright (c) 2013 Hacker's Cafe. All rights reserved.
//
#import "MMRemoteClass.h"
@interface MMTag : MMRemoteClass
@property(nonatomic,retain) NSString *name;
@property(nonatomic,retain) NSString *resource_uri;
@property(nonatomic,retain) NSString *slug;
@end
また、AppDelegate などで、API のベースURLをセットしておきます
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[MMRemoteConfig defaultConfig].baseurl = @"http://example.net/api/";
;
return YES;
}
MMTag.m に、ベースURLの続きと、JSON内のキープロパティを記述します。
//
// MMTag.m
// MoyaMap
//
// Created by Haruyuki Seki on 2/16/13.
// Copyright (c) 2013 Hacker's Cafe. All rights reserved.
//
#import "MMTag.h"
@implementation MMTag
/*
// baseurl に続くURL
*/
+(NSString *)representUrl{
return @"tag";
}
/*
// JSON 内の結果配列用キー
*/
+(NSString *)resultKey{
return @"objects";
}
@end
あとは、データを読み込みたい場所で +(void)fetchAsync: を呼び、ブロックを渡すだけで、非同期でオブジェクト化までしてくれます。
- (void)viewDidLoad
{
[super viewDidLoad];
[MMTag fetchAsync:^(NSArray *allRemote, NSError *error) {
// allRemote に MMTag の配列が入っているので好きなようにする
if (!error){
for (MMTag *tag in allRemote){
NSLog(@"name is %@", tag.name);
}
}
}];
}
MMRemoteClass と MMRemoteConfig のソースコードはこんな感じになりました。AFNetworking と、先日作った MMPropertyUtil を使っています。
(MMPropertyUtil の記事はこの記事執筆後カテゴリクラスに変更していますので、この記事のままでは使えません。ご注意ください。)
//
// MMRemoteClass.h
// MoyaMap
//
// Created by Haruyuki Seki on 2/16/13.
// Copyright (c) 2013 Hacker's Cafe. All rights reserved.
//
#import <Foundation/Foundation.h>
// bloc definition
typedef void(^MMFetchCompletionBlock)(NSArray *allRemote, NSError *error);
@interface MMRemoteClass : NSObject
@property (nonatomic, strong) NSNumber* remoteId; // id property of remote class
/*
// read JSON data from server and call block with NSArray of instanced Classes.
*/
+(void)fetchAsync:(MMFetchCompletionBlock)completionBlock;
/*
// you should override below methods on a subclass
*/
+(NSString *)representUrl;
+(NSString *)resultKey;
@end
//
// MMRemoteClass.m
// MoyaMap
//
// Created by Haruyuki Seki on 2/16/13.
// Copyright (c) 2013 Hacker's Cafe. All rights reserved.
//
#import <objc/runtime.h>
#import "MMRemoteClass.h"
#import "MMRemoteConfig.h"
#import "AFNetworking.h"
#import "MMPropertyUtil.h"
@implementation MMRemoteClass
/**
// read data from specified URI
*/
+(void)fetchAsync:(MMFetchCompletionBlock)completionBlock{
// create url
NSString *strurl = [NSString stringWithFormat:@"%@%@", [MMRemoteConfig defaultConfig].baseurl, [[self class] performSelector:@selector(representUrl)]];
NSLog(@"call:%@", strurl);
NSURL *url = [NSURL URLWithString:strurl];
// call URL
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSLog(@"App.net Global Stream: %@", JSON);
// set parsed objects to NSArray
NSArray *ret = [[self class] parseJSONArray:[JSON valueForKeyPath:[[self class] performSelector:@selector(resultKey)]]];
// call block method
completionBlock(ret,nil);
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(@"App.net Error: %@", [error localizedDescription]);
// error handling
completionBlock(nil,error);
}];
[operation start];
}
#pragma mark -
#pragma mark internal method
/**
// parse JSONArray and create class instance
*/
+(NSArray *)parseJSONArray:(NSArray *)array{
NSMutableArray *ret = [NSMutableArray arrayWithCapacity:[array count]];
for (NSDictionary *dict in array){
id obj = [[[self class] alloc] init];
[obj performSelector:@selector(setPropertiesUsingRemoteDictionary:) withObject:dict];
[ret addObject:obj];
}
return [[NSArray alloc] initWithArray:ret];
}
/*
// set property from JSON dictionary
*/
- (void) setPropertiesUsingRemoteDictionary:(NSDictionary *)dict
{
if ([dict objectForKey:@"id"]){
self.remoteId = [dict objectForKey:@"id"];
}
NSDictionary *props = [MMPropertyUtil classPropsFor:[self class]];
for (NSString *key in [props allKeys]){
NSLog(@"key:%@", key);
if ([dict objectForKey:key]){
[self setValue:[dict objectForKey:key] forKey:key];
}
}
}
#pragma mark -
#pragma mark below methods shoul be doverride in a subclass
+(NSString *)representUrl{
[self doesNotRecognizeSelector:_cmd];
return nil;
}
+(NSString *)resultKey{
[self doesNotRecognizeSelector:_cmd];
return nil;
}
@end
//
// MMRemoteConfig.h
// MoyaMap
//
// Created by Haruyuki Seki on 2/16/13.
// Copyright (c) 2013 Hacker's Cafe. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface MMRemoteConfig : NSObject
@property(nonatomic,strong) NSString *baseurl;
+ (MMRemoteConfig *) defaultConfig;
@end
//
// MMRemoteConfig.m
// MoyaMap
//
// usage:
// [MMRemoteConfig defaultConfig].baseurl = @"http://example.net/api/";
//
// Created by Haruyuki Seki on 2/16/13.
// Copyright (c) 2013 Hacker's Cafe. All rights reserved.
//
#import "MMRemoteConfig.h"
@implementation MMRemoteConfig
static MMRemoteConfig *defaultConfig = nil;
/*
// singleton
*/
+ (MMRemoteConfig *) defaultConfig
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
MMRemoteConfig *newConfig = [[MMRemoteConfig alloc] init];
[newConfig useAsDefault];
});
return defaultConfig;
}
- (void) useAsDefault
{
defaultConfig = self;
}
@end
これで、JSONの形に応じて様々なクラスのインスタンス化がしやすくなりました。
このクラスの拡張アイデアとしては、以下のようなことが考えられます。
- URLパラメータを渡せるようにし、動的なURLを呼び出せるようにする
- 配列以外の形式のJSONデータも読めるようにする(いきなりDictionaryが帰ってくる場合など)
- タイムアウト処理などを設定できるようにする(MMRemoteConfig でセットする)
- ネストした class への対応(NSRailsは対応している)
- CoreData 対応(NSRailsは対応している)
- CocoaPods 対応