mixi engineer blog

*** 引っ越しました。最新の情報はこちら → https://medium.com/mixi-developers *** ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

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];

alertCallback1delegate メソッドに代わるブロックになっています。あるいは何の変数代入もせずこんな風に書くのも良いでしょう:

    // アラート表示
    [[[[UIAlertView alloc] 
       initWithTitle:@"Alert1"
       message:@"..."
       callback:^(NSInteger buttonIndex) {
           if(buttonIndex == 0) {
               ... // [キャンセル]処理
           } else {
               ... // [OK]処理
           }
       }
       cancelButtonTitle:@"キャンセル"
       otherButtonTitles:@"OK", nil]
      autorelease]
     show];

アラートの生成とその後の処理を同じところにまとめて書けるのがいいですし、UIViewControllerUIAlertViewDelegateプロトコルを実装する必要もなくなりとてもスッキリします。 という訳で早速この 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_ttypedef しました。これがクリックされたボタンのインデックスを引数に取るコールバックブロックの型になります。

さて、実装はどうしましょう。このコールバックを呼ぶための delegate インスタンスを作って UIAlertView に持たせたい訳ですが、そのメモリ管理をどう行うかが問題になります。UIAlertViewdelegate プロパティは 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 プログラミングの面白いところだと思います。

ではまた次回、お会いしましょう。