2013/03/04更新

[XCODE] 処理が終わった際のコールバックをSelectorやBlockで実装する方法

このエントリーをはてなブックマークに追加            

こんにちは、@yoheiMuneです。
今日はObjective-Cを用いた実装で、処理が終わった際に呼び出し元を呼び出す方法として、SelectorとBlockの実装をブログに書きたいと思います。

画像


コールバックをSelectorやBlockで実装するとは

node.jsやjavaScriptのXHRを用いた実装では一般的なアーキテクチャで、 HTTP通信やファイルIOなどの処理が終わったら、結果を呼び出し元に通知するコールバックの実装をObjective-Cで実装する方法を記載した記事となります。

HTTP通信やファイルIOは時間のかかる処理で、その処理をメインスレッドで行う(=同期で処理する)と、UI描画が止まってしまい、残念なアプリになってしまいます。
それに対応するため、HTTP通信やファイルIOはサブスレッドで行い、終わったら呼び出し元に通知するコールバック型のアーキテクチャがいい感じに機能します。

今回のブログでは、コールバックアーキテクチャをSelectorとBlockとそれぞれで行う方法を記載したいと思います。



Selectorを用いてコールバックアーキテクチャを実装する

まずはSelectorを用いたコールバックアーキテクチャです。
Selector(=Javaでいうとjava.lang.Methodみたいなやつ)とそのメソッド保持インスタンスをコールバックに指定して、 処理が終わったら呼び出してもらいます。
呼び出し側の実装例は、以下のようになります。
(呼び出し側)
// .hファイル
#import <UIKit/UIKit.h>
#import "SomeWorker.h"
@interface CallbackViewController : UIViewController {
  
  // 呼び出すWorkerのインスタンス
  SomeWorker *someWorker;
}

// なんらかの処理を行うメソッド。
- (void)callSomeWorker;

// コールバックで呼んでもらうメソッド
-(void)callbackFprWorkWithResult:(NSDictionary *)resultDictionary error:(NSError *)error;
@end


// .mファイル
#import CallbackViewController
@implementation CallbackViewController
- (void)callSomeWorker {
  
  // Selectorを利用する場合
  // doSomeWorkWithの引数に、self(コールバック処理を保有するインスタンス)と
  // selector(コールバック処理)を指定します。
  someWorker = [[SomeWorker alloc] init];
  [someWorker doSomeWorkWith:self selector:@selector(callbackFprWorkWithResult:error:)];
}

// コールバックで呼んでもらうメソッドを実装します。
-(void)callbackFprWorkWithResult:(NSDictionary *)resultDictionary error:(NSError *)error {
  NSLog(@"callbackForWorkWithResult:error: is called.");
  NSLog(@"resultDictonary = %@", resultDictionary);
  NSLog(@"error = %@", error);
}
@end
これで呼び出し側の実装が完了です。 @selector(...)という記述が慣れない感じですが、それ以外は普通な感じです。


続いて呼び出されるSomeWorkerクラスの実装です。
idと@selectorの受け取り方、呼び出し方がポイントとなります。
(呼び出される側)
// ヘッダーファイル(.hファイル)
@interface SomeWorker : NSObject
// idとselectorを引数で受け取るメソッドを定義します。
-(void)doSomeWorkWith:(id)callbackInstance selector:(SEL)callbackSelector;
@end

// 実装ファイル(.mファイル)
#import "SomeWorker.h"
@implementation SomeWorker
// 以下のpragma...は、ARC利用時に出るワーニングを抑止するもの。
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
-(void)doSomeWorkWith:(id)callbackInstance selector:(SEL)callbackSelector {
  

  // 何か処理を行う。HTTP通信やファイル扱いなど。
  NSError *error = nil;
  NSMutableDictionary *dictonary = /*なんらかの処理結果を受け取る*/;

  // コールバックが指定されている場合には、コールバックを行う。
  if (callbackInstance && callbackSelector) {
    NSMethodSignature *sig = [callbackInstance methodSignatureForSelector:callbackSelector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
    [invocation setSelector:callbackSelector];
    [invocation setTarget:callbackInstance];
    // コールバックの引数に値を指定する場合には、index >=2を指定する。
    [invocation setArgument:&dictonary atIndex:2];
    [invocation setArgument:&error atIndex:3];
    [invocation invoke];
  }

  // callback.
  // 引数が2つまでの場合には、以下のように簡単にも書ける。
//   if (callbackInstance && callbackSelector) {
//    [callbackInstance performSelector:callbackSelector withObject:dictonary withObject:error];
//  }
}
@end
こんな感じで処理を記述することで、idとselectorを利用してコールバック処理を記述することが出来ます。
インラインコメントにも書きましたが、HTTP通信やファイルダウンロード、ファイル操作、DB操作などをUIに影響させないように非同期で行って、 その結果が分かったらコールバックしてもらうなどに使うと便利な仕組みです。



Blockを用いたコールバックを実装する

続いてBlockを用いたコールバック処理の実装方法です。
呼び出し先のメソッドの引数に、Block(処理のまとまり)を渡して、処理が終わり次第そのBlockを呼び出してもらう感じです。
呼び出し側の実装は、以下のような感じになります。。
(呼び出し側)
// ヘッダファイル(.hファイル)
#import <UIKit/UIKit.h>
#import "SomeWorker.h"
@interface CallbackViewController : UIViewController {
  SomeWorker *someWorker;
}
// なんらかの処理を行うメソッド。
- (void)callSomeWorker;
@end

// 実装ファイル(.mファイル)
#import CallbackViewController
@implementation CallbackViewController
- (void)callSomeWorker {
  
  // Selectorを利用する場合
  // doSomeWorkWithの引数に、self(コールバック処理を保有するインスタンス)と
  // selector(コールバック処理)を指定します。
  someWorker = [[SomeWorker alloc] init];
  [someWorker doSomeWorkWith:^(NSDictionary *resultDictonary, NSError *error) {

    // ここの中がコールバック処理。
    // 引数で結果(resultDictionary)とエラー内容(error)を受け取る。
    NSLog(@"CallbackHandler called.");
    NSLog(@"resultDictonary = %@", resultDictonary);
    NSLog(@"error = %@", error);
  }];
}
@end
Blockで実装した場合には、コールバック処理用のメソッドをわざわざ準備しなくていいので、楽チンです。
個人的には、呼び出し処理と完了処理が近くに記述されるので、読みやすいなぁと思ってます。
ただ、Blockの構造はObjective-C特有なので、慣れないとですがw。


続いて呼び出される側の実装です。
Blockは使えない環境もあるようなので、利用できるかを確認するコードも含めて、実装を行います。
(呼び出される側)
// ヘッダファイル(.hファイル)

// NS_BLOCKS_AVAILABLEで、Blockが利用可能か否かを判断できます。
// 利用可能な場合のみ、今回利用するコールバック関数用の型を定義します。
#if NS_BLOCKS_AVAILABLE
// 型定義をすると、Blockの長ったらしい型名を毎回書く手間を簡略化することが出来ます。
// 型定義では引数の変数名まで指定すると、コールバックを呼び出す側の実装で、コード保管が効いて便利です。
typedef void (^CallbackHandler)(NSDictionary * resultDictonary, NSError *error);
#endif
@interface SomeWorker : NSObject
#if NS_BLOCKS_AVAILABLE
// 引数にBlockのコールバック関数を受け取る処理を定義します。
-(void)doSomeWorkWith:(CallbackHandler)handler;
#endif
@end


// 実装ファイル(.mファイル)
#import "SomeWorker.h"
@implementation SomeWorker

// 引数にBlockのコールバック関数を受け取る処理
-(void)doSomeWorkWith:(CallbackHandler)handler {

  // 何らかの処理を行います。
  // ここではコールバックに渡す内容のインスタンスを作成しているのみ。
  NSError *error = nil;
  NSMutableDictionary *dictonary = [NSMutableDictionary dictionary];

  
  // コールバックが指定された場合には、それを呼び出す。
  if (handler) {
    handler(dictonary, error);
  }
}
@end
こんな感じでBlockを用いたコールバック型の処理を記述することが出来ます。
Blockの型定義が独特な形をしている点が慣れないのですが、コールバックを呼び出す部分はC言語の関数のように呼び出せるので、すごく奇麗に書けます。
また、Selectorを用いた場合とは異なり、Blockの型定義をしているおかげで、Blockの呼び出し時に異なる型を指定すると、コンパイラからワーニングがでます。



最後に

自分は最初は上記2つのやり方を知らずに実装していたので大変な感じでしたが、コールバック方法がわかって奇麗に書けるようになってきました。
Objective-Cの実装をしていると、言語レベルでもフレームワークレベルでもたくさん学ぶことがあって、ありがたい限りです。 今後も良い内容があればブログに書いていきたいと思います。

最後までご覧頂きましてありがとうございました。





こんな記事もいかがですか?

RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。