iOSコーディングスタイルを変えてしまうBlocksKitの紹介

iOS4からBlockが導入されました。利用目的としてはDelegateパターンと大差ないと思っていますが、処理記述が呼び出し元で記述できることで可読性が高くなります。
また、コールバック時に利用されるperformedSelectorが非常に使いづらく複数の引数となった場合などを考慮してNSDictionaryで定義したりInvocationを使う必要がったりと面倒な点が多々ありました。

今回は、CoreFoundationにカテゴリで組み込まれたBlockを活用したBlocksKitフレームワークを紹介したいと思います。かなり便利なため、今後のコーディングスタイルを変えてしまうほどのインパクトがあると個人的には思っています。

  • BlocksKit

https://github.com/zwaldowski/BlocksKit

BlocksKitはBSD, MITライセンスの元で利用可能です。Blockを使うので当然ながら、iOS4.0以上、MacOSX 10.6以上となります。BlocksKitの内部で別プロジェクトA2DynamicDelegateというモジュールが必要となります。

  • A2DynamicDelegate

https://github.com/pandamonia/A2DynamicDelegate


BlocksKit導入手順


BlocksKitはFrameworkとしても導入できますが、ここでは静的ライブラリとして導入してみます。

1. ソースのダウンロード
上部のBlocksKit、A2DynamicDelegateからプロジェクトをダウンロードします。
2. BlocksKitにA2BlockDelegateを追加
BlocksKitにA2DynamicDelegateとA2BlockDelegateのモジュールがないので「BlocksKit/A2DynamicDelegate/」にA2BlockDelegateとA2DynamicDelegateをコピーします。プロジェクト上でリンクが切れている場合は、リンクを消しXcodeからファイルを追加します。
3. 自ブロジェクトフォルダにBlocksKitフォルダを追加
ここでは「自プロジェクトフォルダ/External/BlocksKit/」に配置しました。
4. 自プロジェクトにBlocksKitプロジェクトを追加
自分のプロジェクトにを開き、BlocksKit.xcodeprojをプロジェクトナビゲータにドロップして、BlocksKitプロジェクト毎追加します。
5. リンクライブラリにlibBlocksKit.a、MessageUI.frameworkを追加
自分のプロジェクトのターゲットのLink Binary With Librariesに libBlocksKit.a とMessageUI.framework を追加します。


6. その他リンカフラグに「-ObjC -all_load」を追加
自分のプロジェクトターゲットのBuild Settingsの Other Linker Flags に ObjC -all_load を追加します。


7. ヘッダ検索パスにBlocksKitを設定
自分のプロジェクトターゲットのBuild Settingsの Header Search Paths に"$(SRCROOT)/External/BlocksKit" を追加し、Recursiveにチェックをつけます。


8. pchにBlocksKitをimport
全ソースから呼び出せるように自分のプロジェクトのPrefix.pchに #import を追加する

MyBlocks-Prefix.pch

#import <Availability.h>

#ifndef __IPHONE_4_0
#warning "This project uses features only available in iOS SDK 4.0 and later."
#endif

#ifdef __OBJC__
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
    #import <BlocksKit/BlocksKit.h>
#endif

BlocksKitの利便性と便利な使い方

UIActionSheet+BlocksKit

UIActionSheetは delegateを指定し UIActionSheet#actionSheet:clickedButtonAtIndex でハンドリングします制御しますが、Controllerに複数のUIActionSheetが存在する場合は、インスタンス変数を利用したりTagを利用してDelegateメソッド内で分岐して記述する必要があります。また、タップされたButtonのIndexで管理しているために、条件によってボタン数が変わる場合などは表示する処理とタップ時の処理の箇所に、同じIndexに調整する処理を記述しなければなりませんでした。

ブロックを使わない場合のコード

- (void)showSheet1 {
     self.sheet1 = [[[UIActionSheet alloc] init] autorelease];
     _sheet1.title = @"Sheet";
     _sheet1.delegate = self;
     if (condition1) {
          [_sheet1 addButtonWithTitle:@"Action1"];
     }
     if (condition2) {
          [_sheet1 addButtonWithTitle:@"Action2"];
     }
     [_sheet1 addButtonWithTitle:@"Close"];
     [_sheet1 showInView:self.view];
}
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
     if (actionSheet == _sheet1) {
          NSInteger adjust1 = (condition1) ? 0 : 1;
          NSInteger adjust2 = (condition2) ? 0 : 1;
          if (buttonIndex == (0 - adjust1)) {
               //Action1の処理
          } else if (buttonIndex == (1 - adjust1 - adjust2)) {
               //Action2の処理
          } else if (buttonIndex == (2 - adjust1 - adjust2)) {
               //Close
          }
     }
     
     if (actionSheet == _sheet2) {          //sheet2の処理
     }
}


UIActionSheet+BlocksKitを使用したコード

- (void)showSheet1 {     UIActionSheet *sheet1 = [UIActionSheet actionSheetWithTitle:@"Sheet"];
     if (condition1) {
          [sheet1 addButtonWithTitle:@"Action1" handler:^(void) {
               //Action1の処理
          }];

     }
     if (condition2) {
          [sheet1 addButtonWithTitle:@"Action2" handler:^(void) {
               //Action2の処理
          }];

     }
     [sheet1 setCancelButtonIndex:[sheet addButtonWithTitle:@"Close"]];
     [sheet1 showInView:self.view];
}

ActionSheetの表示と同時に押下時の処理まで同じメソッド内に記述可能なので、ButtonIndexを管理しなくともよくなり、可読性も飛躍的に向上すると思います。


UIAlertView+BlocksKit

UIActionSheetと同じように、ボタン押下時の処理を表示時に記述できます。

UIAlertView *alert = [UIAlertView alertViewWithTitle:@"Alert" message:@"UIAlertView Test"];
[alert addButtonWithTitle:@"Action" handler:^(void) {
     NSLog(@"Action");
}];
[alert addButtonWithTitle:@"Close"];
[alert show];


UIAlertViewのボタン押下後に、続けてUIAlertViewを表示することも簡単に記述できます。

UIAlertView *alert1 = [UIAlertView alertViewWithTitle:@"Alert1" message:@"連続Alert表示"];
[alert1 addButtonWithTitle:@"Show Alert2" handler:^(void) {
     UIAlertView *alert2 = [UIAlertView alertViewWithTitle:@"Alert2"];
     [alert2 addButtonWithTitle:@"Show Alert3" handler:^(void) {
          UIAlertView *alert3 = [UIAlertView alertViewWithTitle:@"Alert3"];
          [alert3 addButtonWithTitle:@"Close"];
          [alert3 show];
     }];
     [alert2 addButtonWithTitle:@"Close"];
     [alert2 show];
}];
[alert1 addButtonWithTitle:@"Close"];
[alert1 show];
NSURLConnection+BlocksKit

通常、NSURLConnectionで実装する場合、delegateによりNSURLConnectionDelegate、NSURLConnectionDataDelegate などの複数のメソッドを実装してコールバックさせる必要があり結構な手間がかかります。
BlocksKitでは通信の処理を包括されているので、かなり簡潔に記述することが可能です。またリクエスト発行と同時に受信時の処理が同時にかけるので飛躍的に可読性は上がります。

NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://d.hatena.ne.jp/h_mori/"]];
[NSURLConnection startConnectionWithRequest:req
                             successHandler:^(NSURLConnection *con, NSURLResponse *res, NSData *data){
                                 NSString *html = [[[NSString alloc] initWithData:data encoding:NSJapaneseEUCStringEncoding] autorelease];
                                 NSLog(@"html=%@", html);
                             }
                             failureHandler:^(NSURLConnection *con, NSError *err){
                                  NSLog(@"NSURLConnection failed : %@", [err localizedDescription]);
                             }];
UIWebView+BlocksKit

UIWebViewも複数のdelegateメソッドを実装しますが、BlocksKitではUIWebViewのリクエスト時に受信時の処理を書くことができます。

UIWebView *webView = [[[UIWebView alloc] init] autorelease];
webView.didFinishLoadBlock = ^(UIWebView *v) {
     NSLog(@"didFinishLoadBlock");
};
webView.didFinishWithErrorBlock = ^(UIWebView *v, NSError *err) {
     NSLog(@"didFinishWithErrorBlock");
};
[webView.dynamicDelegate webViewDidFinishLoad:webView];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://d.hatena.ne.jp/h_mori/"]]];
MFMailComposeViewController+BlocksKit

メーラーコンポーネントのMailComposerを使った場合も通常通りに実装すると処理部がバラバラとなりがちですが、BlocksKitを使うと簡潔に記述が可能です。

MFMailComposeViewController *ctl = [[[MFMailComposeViewController alloc] init] autorelease];
ctl.mailComposeDelegate = self;
ctl.completionBlock = ^(MFMailComposeViewController *controller, MFMailComposeResult result, NSError *err){
    if (result == MFMailComposeResultSent) {
        UIAlertView *a = [UIAlertView alertViewWithTitle:@"MFMailComposeViewController" message:@"sent mail."];
        [a addButtonWithTitle:@"Close"];
        [a show];
    }
};
[self presentModalViewController:ctl animated:YES];
UIControl+BlocksKit

基本的にはtarget、selectorの代替としてBlockで記述するのですが、コントロール設置と同時にそのアクションの処理がかけるのはやはり便利です。

UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame = CGRectMake(40, 20, 240, 44);
[button setTitle:@"Caption" forState:UIControlStateNormal];
[button addEventHandler:^(id sender) {
     //Action
} forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];


UIBarButtonItem+BlocksKit
UIControl+BlocksKitと同じでtarget、selectorの代わりにBlockを記述します。

self.navigationItem.leftBarButtonItem =
[[[UIBarButtonItem alloc]
     initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
     handler:^(id sender) {
          //Cancel Action
     }] autorelease];
UIGestureRecognizer+BlocksKit

GestrueRecognizerもBlocksKitで拡張されています。例ではスワイプをハンドリングしています。

UISwipeGestureRecognizer *swipe =
[[UISwipeGestureRecognizer alloc]
     initWithHandler:^(UIGestureRecognizer *sender, UIGestureRecognizerState state, CGPoint location){
          //Swipe Action
     }];
[self.view addGestureRecognizer:swipe];
NSObject+BlocksKit

基本的にはperformSelectorの代替ですが、performBlock:afterDelay: cancelBlock: が拡張されています。

[NSObject performBlock:^(void) {
    NSLog(@"fire");
} afterDelay:1];
NSTimer+BlocksKit

これも基本的にはtargetとselectorの代替です。

[NSTimer
 scheduledTimerWithTimeInterval:1
 block:^(NSTimeInterval time) {
     //action every 1 sec.
 }
 repeats:YES];

ブロック実装上での注意点

ブロックはスコープ外の変数にアクセスできるように設計されているため、ブロック自身とブロック内で利用される変数はヒープメモリ領域にコピーされ、ブロック自身がブロック内で参照する変数を強参照を保持(retain)するようになります。通常ブロックは自コントローラ等で強参照で保持(retain)すると思いますが、ブロック内でselfや自コントローラ内の強参照のインスタンス変数を直接参照すると循環参照が発生し、メモリリークを引き起こします。

ブロックによる循環参照の例(メモリリーク
UIButton *sheetButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[sheetButton setTitle:@"UIActionSheet" forState:UIControlStateNormal];
[sheetButton addEventHandler:^(id sender) {
    UIActionSheet *sheet1 = [UIActionSheet actionSheetWithTitle:@"Sheet1"];
    [sheet1 setCancelButtonIndex:[sheet1 addButtonWithTitle:@"Close"]];
    [sheet1 showInView:self.view];  //ブロック内でselfを直参照
} forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:sheetButton];

この例では self > view > sheetButton > (block) > self と強参照されています。なのでこのコントローラのdeallocは永久に呼び出さずメモリリークとなります。


この循環参照を断ち切るにはブロックのスコープ外の変数にアクセスする場合に __block 指定子を利用し、弱参照にする必要があります。
iOS5で導入されたARC(Auto Reference Counting)が有効の場合は更に弱参照修飾子 (__weak) を使う必要があります。

ブロックによる循環参照の回避コード (※ARC無効)
__block MBBlocksViewController *weakSelf = self; // 循環参照回避のweakSelfを定義
UIButton *sheetButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[sheetButton setTitle:@"UIActionSheet" forState:UIControlStateNormal];
[sheetButton addEventHandler:^(id sender) {
    UIActionSheet *sheet1 = [UIActionSheet actionSheetWithTitle:@"Sheet1"];
    [sheet1 setCancelButtonIndex:[sheet1 addButtonWithTitle:@"Close"]];
    [sheet1 showInView:weakSelf.view]; // weakSelfを参照するように変更
} forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:sheetButton];
ARC有効の場合
__block __weak MBBlocksViewController *weakSelf = self; //__weak修飾子 で定義


ブロックによる循環参照はコピペ等で簡単に発生してしまうので注意する必要があります。私の場合はブロックを利用する場合は dealloc にBreakPointを設定し、毎回開放されることを確認するようにしています。以前「※サウンドアクションによるデバッグ」で紹介した方法で、BreakPointの設定を変更し音のみの通知を行うようにすると楽になると思います。


サンプルソース

今回、BlocksKitを導入して色々と実験したソースをGithubにアップしました。

https://github.com/hmori/MyBlocks


MBBlocksViewControllerでは、Viewを配置するloadViewだけですべての処理をコーディングしてみました。ちょっと無理やり感がありますが処理が纏まっているので可読性はそう悪くないと思います。



この便利さを経験するとBlocksKit無しでは開発できなくなりそうです。新しいプロジェクトを開始する際に最初に導入するツールの定番になるかもしれないですね。