読者です 読者をやめる 読者になる 読者になる

最近よくやってる Blocks の使い方

iOS cocoa iPhone

Blocks 怖いような、便利なような。そんな物ですがみなさんはどんな感じに使っていますか?
僕は最近こんな感じのメソッドを実装して使ってます。

UINavigationController *nvc = [DetailViewController navigationBasedViewControllerWithConfigurationBlock:^(DetailViewController *viewController) {
        NSDate *object = _objects[indexPath.row];
        viewController.detailItem = object;
}];
[self.navigationController presentViewController:nvc animated:YES completion:NULL];

特に凄く便利かというとそうでもないと思うんですけど、典型的なモーダルな ViewController を表示させるコードってこんな感じだと思います。

DetailViewController *vc = [[DetailViewController alloc] init];
NSDate *object = _objects[indexPath.row];
vc.detailItem = object;
UINavigationController *nvc = [[UINavigationController alloc] initWithRootViewController:vc];
[self.navigationController presentViewController:nvc animated:YES completion:NULL];

要するにこんなことをしてるんですよね。

  • 表示させたい ViewController クラスを生成
  • 表示させたい ViewController クラスに何か値をセット
  • NavigationController を生成
  • 表示

いろいろとタイプすることが多いのをちょっとだけ省略したくてこうしてます。

実装はこんな感じです

+ (UINavigationController *)navigationBasedViewControllerWithConfigurationBlock:(void(^)(id viewController))configurationBlock
{
    UIViewController *vc = [[[self class] alloc] init];
    if (configurationBlock) {
        configurationBlock(vc);
    }
    return [[UINavigationController alloc] initWithRootViewController:vc];
}

引数で受けた Block は同期的な感じで使うのでコピーしてないので循環参照を気にする必要もないですね。 この例では UIViewController の生成を init にしちゃってますがそのあたりは実際の実装との相談でバリエーションを持たせてもいいかもしれないです。 Block に渡す引数の型は id 型にしておけば、呼び出し側で任意のクラス型に書き換えられるので、ブロック内でキャストする必要もないです。 カテゴリメソッドとして実装するとある程度汎用的に使えます。

思いつきで進化版

もっと発展させて、さっき思いつきで実装したカテゴリメソッドはこんな感じで使います。

__weak typeof(self) weakSelf = self;
UINavigationController *nvc = [DetailViewController cs_navigationBasedViewControllerWithConfigurationBlock:^(DetailViewController *viewController) {
    NSDate *object = _objects[indexPath.row];
    viewController.detailItem = object;
} wantsDismiss:^(DetailViewController *viewController) {
    [weakSelf.navigationController dismissViewControllerAnimated:YES completion:^{
        // do something
        NSLog(@"%s dismissViewControllerAnimated: %@", __PRETTY_FUNCTION__, weakSelf);
    }];
}];

昔、師匠に教えてもらったんですけど「モーダルを閉じるのはモーダルを出した側がやる方がいい」と。Apple のサンプルでも Delegate 経由でモーダルを出した側からモーダルを閉じるようにしているのがあります。あれって実装するのが面倒くさくてついついモーダル側で自身を閉じてしまいがちでした。このカテゴリメソッドではモーダル側の例えば cancel ボタンなりを押されたときにインスタンス生成時に渡された Block を呼ぶことで呼び出し側に閉じるか否かを委譲させています。さすがに今回は引数として受けた wantsDismiss Blcok は Copy して保持しているので循環参照対策として weak な self を使っています。実装はこんな感じです。

#import "UIViewController+CSAdditions.h"
#import <objc/runtime.h>

static char kUIViewControllerCSAdditionsKey;

@implementation UIViewController (CSAdditions)
+ (UINavigationController *)cs_navigationBasedViewControllerWithConfigurationBlock:(void(^)(id viewController))configurationBlock
{
    UIViewController *vc = [[[self class] alloc] init];
    if (configurationBlock) {
        configurationBlock(vc);
    }
    return [[UINavigationController alloc] initWithRootViewController:vc];
}

+ (UINavigationController *)cs_navigationBasedViewControllerWithConfigurationBlock:(void(^)(id viewController))configurationBlock
                                                                       wantsDismiss:(void(^)(id viewController))callbackBlock
{
    UIViewController *vc = [[[self class] alloc] init];
    if (configurationBlock) {
        configurationBlock(vc);
    }
    
    if (callbackBlock) {
        [vc cs_setWantsDismisCallbackBlock:callbackBlock];
    }
    
    return [[UINavigationController alloc] initWithRootViewController:vc];
}

- (void)cs_setWantsDismisCallbackBlock:(void(^)(id viewController))callbackBlock
{
    objc_setAssociatedObject(self,
                             &kUIViewControllerCSAdditionsKey,
                             callbackBlock,
                             OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void(^)(id viewController))cs_wantsDismisCallbackBlock;
{
    return (void(^)(id viewController))objc_getAssociatedObject(self, &kUIViewControllerCSAdditionsKey);
}
// DetailViewController 側の使用例
// DetailViewController の leftBarButtonItem に設置した Cancel ボタンを押したときに呼ばれるメソッド
- (void)wantsDismis:(id)sender
{
    /// 登録されている Block を呼び出して引数として自身をわたす
    [self cs_wantsDismisCallbackBlock](self);
}

associatedObject を使って wantsDismisCallbackBlock を保持するようにしています。保持した wantsDismisCallbackBlock の解放をどうしようかと思いましたが、明示的に解放しなくても自分自身が解放されるときにいっしょに解放してくれるようだったので特に何もしていません。

Blocks ってやっぱり便利

とはいえ、Blocks は万能ではないので traditional な delegate パターンや target-action パターンなんかもうまく活用していきたいですね。