Edited at

Objective-C 非同期処理のCallbackをBlocksで同時に渡す

More than 5 years have passed since last update.

非同期処理を完了したあとの処理をdelegateでやっていたが、Blocksを使って非同期処理メソッドの引数に完了処理を渡すようにしてプログラムをわかりやすくしたい。SDKでやっているクラスを参考にしよう。


SDKのCompletionHandlerの使用例

CompletionHandlerでSDKを検索。blocksの宣言は可読性がないので以下のようにtypedefしているパターンが多い。

typedef void (^CLGeocodeCompletionHandler)(NSArray *placemark, NSError *error);

- (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler

SLComposeViewControllerのようにプロパティで持てるようにしているものもあった。


SLComposeViewController.h

typedef void (^SLComposeViewControllerCompletionHandler)(SLComposeViewControllerResult result); 

@property (nonatomic, copy) SLComposeViewControllerCompletionHandler completionHandler

個人的にはメソッド呼び出し時に終了処理をかけないのは魅力半減なので呼び出しメソッドの引数にblockを渡せる方がいい。

というわけで流れとしては


1. typepefする

typedef void (^MYCompletionHandler)(NSError *error);


2. メソッドを宣言する

SDKを真似る。

- (void)executeAsyncWithCompletionHandle:(MYCompletionHandler)completionHandler;

- (void)executeAsyncWithObject:(id)obj completionHandler:(MYCompletionHandler)completionHandler;


3. メソッドを実装する

SDKのコードは見れないので想像。ARC有効。どのqueueで非同期処理するかは処理に応じて考えるべし。

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
[self executeAsyncWithCompletionHandler:^(NSError *error) {
NSLog(@"in CompletionHandle");
}];
}

- (void)executeSync:(NSError **)error
{
}

- (void)executeAsyncWithCompletionHandler:(MYCompletionHandler)completionHandler;
{
__weak ViewController *me = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
[me executeSync:&error];
completionHandler(error);
});
}

@end


completionHandlerはどのqueueで呼び出すか?

completionHandlerはどのスレッドで処理するのかが悩みどころ。試しに以下のようにdispatch_get_current_queueを使って呼び出しスレッドで処理するようにしてみようとしたらdispatch_get_current_queueはiOS6では非推奨になっていた。

- (void)executeAsyncWithCompletionHandler:(MYCompletionHandler)completionHandler;

{
__weak ViewController *me = self;
// 'dispatch_get_current_queue' is deprecated: first deprecated in iOS 6.0
dispatch_queue_t queue = dispatch_get_current_queue();
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
[me executeSync:&error];
dispatch_async(queue, ^{
completionHandler(error);
});
});
}

dispatch_get_current_queueを使わないとするとなんらかのqueueを明示的に指定せざるをえない。

非同期処理はそのクラス内部で別スレッドを起こしているからコールバックをどのスレッドで処理するのを考えるかは呼び出し元の責任・・・、と今まで考えていたのだがglobal_main_queueでcompletionHandlerを呼び出してあげたほうがいいのだろうか?

と書いていたらコメントを頂いたので追記。SDKの中には受け取ったBlockを実行するqueueを引数に取るものがある。

// NSURLConnection.h

+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler

// GLKTextureLoader.h
- (void)cubeMapWithContentsOfFile:(NSString *)fileName options:(NSDictionary *)textureOperations queue:(dispatch_queue_t)queue completionHandler:(GLKTextureLoaderCallback)block

GLKTextureLoaderなどではqueueがNULLならmain dispatch queueで実行するよと書いてある。これを参考にすると以下のように書ける。このほうが呼び出す側は安心はできる。

- (void)viewDidLoad

{
[super viewDidLoad];

[self executeAsyncOnQueue:dispatch_get_main_queue() completionHandle:^(NSError *error) {
NSLog(@"in CompletionHandle");
}];
}

- (void)executeAsyncOnQueue:(dispatch_queue_t)queue completionHandle:(MYCompletionHandler)completionHandler;
{
__weak ViewController *me = self;
dispatch_async(queue, ^{
NSError *error = nil;
[me executeSync:&error];
completionHandler(error);
});
}