UIAlertView の delegate 束縛を解放しよう
どうも、佐野です。今回は UIAlertView
の拡張カテゴリを作って、delegate
ではなく Block
でコールバック処理を記述するための拡張カテゴリの作り方を紹介します。
iOS4 から Objective-C では Blocks という独自のクロージャ機能が搭載されました。これによってアニメーションの記述やコールバック処理などグッと直観的・効率的に記述できるようになったのですが、残念ながら UIKit によって提供されているクラスの多くはまだ Blocks に最適化された作りになっていません。UIAlertView
もそのひとつです。
UIAlertView
は、その delegate
を実装することでユーザアクションに対する処理を記述する訳ですが、どうも使い勝手が悪い。例えば、同じで画面内で2通りの UIAlertView
を表示するような UIViewController
を作る場合、その実装はこんな風になるでしょう:
@implementation MyViewController ... // 1個目のアラートの表示処理 - (IBAction)action1:(id)sender { // アラート生成 UIAlertView *a1 = [[[UIAlertView alloc] initWithTitle:@"Alert1" message:@"..." delegate:self cancelButtonTitle:@"キャンセル" otherButtonTitles:@"OK", nil] autorelease]; // 識別のためのタグを設定 a1.tag = 1; // 表示 [a1 show]; } // 2個目のアラートの表示処理 - (IBAction)action2:(id)sender { // アラート生成 UIAlertView *a2 = [[[UIAlertView alloc] initWithTitle:@"Alert2" message:@"..." delegate:self cancelButtonTitle:@"キャンセル" otherButtonTitles:@"OK", nil] autorelease]; // 識別のためのタグを設定 a2.tag = 2; // 表示 [a2 show]; } ... // アラートのdelegateメソッド // alertViewのtag属性によってアラート1,2を識別し、処理を分岐する - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { // 1個目のアラートに対する処理 if(alertView.tag == 1) { if(buttonIndex == 0) { ... // [キャンセル]処理 } else { ... // [OK]処理 } } // 2個目のアラートに対する処理 else if(alertView.tag == 2) { .... } }
別々の UIAlertView
をわざわざ共通の delegate
メソッドに通してその中で再び分岐させているのが嫌です。これが500行ぐらいのコードになると処理の流れが分散してとても読みづらいものになります。ここはやっぱり Blocks を使って、こんな風にカッコ良く書きたいモノです:
// コールバックブロックを生成 void (^alertCallback1)(NSUInteger) = ^(NSUInteger buttonIndex) { if(buttonIndex == 0) { ... // [キャンセル]処理 } else { ... // [OK]処理 } }; // コールバックを渡してアラートを生成 UIAlertView *a1 = [[[UIAlertView alloc] initWithTitle:@"Alert1" message:@"..." callback: alertCallback1 // ← コールバックブロックを引数に! cancelButtonTitle:@"キャンセル" otherButtonTitles:@"OK", nil] autorelease]; // 表示 [a1 show];
alertCallback1
が delegate
メソッドに代わるブロックになっています。あるいは何の変数代入もせずこんな風に書くのも良いでしょう:
// アラート表示 [[[[UIAlertView alloc] initWithTitle:@"Alert1" message:@"..." callback:^(NSInteger buttonIndex) { if(buttonIndex == 0) { ... // [キャンセル]処理 } else { ... // [OK]処理 } } cancelButtonTitle:@"キャンセル" otherButtonTitles:@"OK", nil] autorelease] show];
アラートの生成とその後の処理を同じところにまとめて書けるのがいいですし、UIViewController
が UIAlertViewDelegate
プロトコルを実装する必要もなくなりとてもスッキリします。 という訳で早速この UIAlertView
拡張カテゴリを作りましょう。まずはヘッダ:
UIAlertView+BlocksExtension.h
@interface UIAlertView(BlocksExtension) typedef void (^UIAlertViewCallback_t)(NSInteger buttonIndex); - (id)initWithTitle:(NSString *)title message:(NSString *)message callback:(UIAlertViewCallback_t)callback cancelButtonTitle:(NSString *)cancelButtonTitle otherButtonTitles:(NSString *)otherButtonTitles, ...; @end
(void (^)(NSInteger buttonIndex))
というブロック型を UIAlertViewCallback_t
と typedef
しました。これがクリックされたボタンのインデックスを引数に取るコールバックブロックの型になります。
さて、実装はどうしましょう。このコールバックを呼ぶための delegate
インスタンスを作って UIAlertView
に持たせたい訳ですが、そのメモリ管理をどう行うかが問題になります。UIAlertView
の delegate
プロパティは assign
属性なので、インスタンスによって保持・解放されません。んーむ、どうしよう。
解決!メモリ管理は誰かにやってもらうという発想を捨て、ここでは delegate
インスタンスが、役割を終えたら自ら消滅してくれるようにします。そのクラスを UIAlertViewCallback
として、まずヘッダはこんな感じ:
UIAlertViewCallback.h
@interface UIAlertViewCallback : NSObject <UIAlertViewDelegate> { UIAlertViewCallback_t callback; } @property (nonatomic, copy) UIAlertViewCallback_t callback; - (id)initWithCallback:(UIAlertViewCallback_t) callback; @end
UIAlertViewDelegate プロトコルの実装を宣言している点に注意してください。実装はこうです:
UIAlertViewCallback.m
@implementation UIAlertViewCallback @synthesize callback; - (id)initWithCallback:(UIAlertViewCallback_t)aCallback { if(self = [super init]) { // コールバックブロックをセット self.callback = aCallback; // 自分自身を保持! [self retain]; } return self; } // UIAlertView の delegate メソッド - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { // コールバックを呼ぶ if(callback) callback(buttonIndex); // コールバックを呼び終えたら自分自身を解放する! [self release]; } - (void)dealloc { self.callback = nil; [super dealloc]; } @end
まずコンストラクタの中で自分自身を保持しています。これによって他のインスタンスからの参照がなくても dealloc
されることはありません。次に UIAlertViewDelegate
の - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
を実装しており、その中でコールバックブロックを呼んで、それが終わったら自分自身を解放しています。こうすることで、インスタンスはちゃんと自分の役割を終えたら消滅してくれる訳です!
これでようやく準備完了、あとはこれを使って UIAlertView
の拡張カテゴリを実装しましょう:
UIAlertView+BlocksExtension.m
@implementation UIAlertView(BlocksExtension) - (id)initWithTitle:(NSString *)title message:(NSString *)message callback:(UIAlertViewCallback_t)callback cancelButtonTitle:(NSString *)cancelButtonTitle otherButtonTitles:(NSString *)otherButtonTitles, ... { self = [self initWithTitle:title message:message delegate:nil cancelButtonTitle:cancelButtonTitle otherButtonTitles:nil]; if(self) { // otherButtonTitles, ... を手動でセット va_list args; va_start(args, otherButtonTitles); for (NSString *arg = otherButtonTitles; arg != nil; arg = va_arg(args, NSString*)) { [self addButtonWithTitle:arg]; } va_end(args); // delegateにUIAlertViewCallbackをセット self.delegate = [[[UIAlertViewCallback alloc] initWithCallback:callback] autorelease]; } return self; } @end
va_list
なんかを使っているのは、可変長引数列をそのまま他のメソッドコールの引数として渡すことができないためです。今回のポイントはその後の self.delegate = [[[UIAlertViewCallback alloc] initWithCallback:callback] autorelease];
と、先ほどのコールバック用のインスタンスを生成してセットしているところです。
どうですか、なかなかクールでしょう。このやり方は UIActionSheet
にもそのまま適用できますし、あとは NSURLConnection
をラッピングして通信コールバックをブロックで記述するようなクラスもこのやり方で作れます。条件は Delegator (UIAlertView
, UIActionSheet
, NSURLConnection
など) が消滅するタイミングで、1度だけ必ずコールされる delegate
メソッドがあることです。だからこそそこで確実に delegate
インスタンスを消滅させることができる訳です。
やり方としてはかなりトリッキーですし、 UIKit がちゃんと Blocks 対応を進めればこの拡張カテゴリは不要になりますが、こうやって開発者が一歩先に進んで低いレイヤーから独自にフレームワークを拡張させることができるのは Objective-C プログラミングの面白いところだと思います。
ではまた次回、お会いしましょう。