PIYO - Tech & Life -

Obj-Cで非同期処理の完了を待ちたいならブロック渡してコールバックだよね

Tips Objective-C iOS

Swiftは出たけどまだまだObjective-Cはオワコンってことにはならないと思うからObjective-Cの話を。

Objective-Cで非同期処理をしてその結果を受けてなんかしたい、ってときが結構あるんだけど待ち方がいまいちよくわからなかったことがあった。今思えば基本的なやり方だけどちゃんと残しておこう。

ちなみにテストなら前に書いたこれでよさそう。アプリケーションコードでやるとスレッド周りで整合性取れなくなるのか正しく動かなかった。それ以上は踏み込んで調べてはいない。

Objective-Cで非同期処理のテスト(依存ライブラリなし版) - ぴよログ

でもよく考えたら非同期処理が終わったことを同期的に知りたいというケースは実はそんなに無い気がしてきたので、最初の非同期処理が終わったあとにやりたい処理も非同期にしてしまえばいい。つまりブロック渡してコールバック。

例としてALAssetsLibraryを使うケースを考える。ALAssetsLibraryを使うクラスは自前のクラスにラップして使うとする。仮に名前をAssetsManagerとかなんとかいう名前にし、loadという読み出し用のメソッドがあるとする。

// AssetsManager.h
- (void)load;
- (UIImage*)latestImage;
+ (AssetsManager*)defaultManager; // Singleton
// AssetsManager.m
- (void)load
{
    _assets = [NSMutableArray new];
    _assetsLibrary = [ALAssetsLibrary new];
    [_assetsLibrary enumerateGroupsWithTypes:ALAssetsSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
        // do something with group
    } failureBlock:^(NSError *error) {
    }];
}

このloadViewController.mをから呼び出し、完了後にUIImageViewを更新するみたいな流れを考える。

// ViewController.m
- (void)updateImage
{
    [[AssetsManager defaultManager] load];
    self.imageView.image = [AssetsManager defaultManager] latestImage];
}

ところがこれではenumerateGroupsWithTypesメソッドの呼び出し後すぐに画像更新の行に到達してしまうためうまくいかない。想定通りの挙動にするためにはenumerateGroupsWithTypesが本当に終わったということを知る必要があるが、終わるのを待つよりもスマートな方法は完了時の処理をブロックで渡してやることになる。

コードで書くとこうなる。

// AssetsManager.h
typedef void (^CompletionBlock)(void);
- (void)loadWithCompletionBlock:(CompletionBlock)block; // 引数としてブロックを渡す
- (UIImage*)latestImage;
+ (AssetsManager*)defaultManager; // Singleton
// AssetsManager.m
- (void)loadWithCompletionBlock:(CompletionBlock)block
{
    _assets = [NSMutableArray new];
    _assetsLibrary = [ALAssetsLibrary new];
    [_assetsLibrary enumerateGroupsWithTypes:ALAssetsSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
        // do something with group
        block(); // ←この行
    } failureBlock:^(NSError *error) {
    }];
}

そしてこの新しくなったloadを使う側はこうなる。

// ViewController.m
- (void)updateImage
{
    [[AssetsManager defaultManager] loadWithCompletionBlock:^{
        self.imageView.image = [AssetsManager defaultManager] latestImage];
    }];
}

これでうまくいく。