iOS3用にUIAlertView,UIActionSheetを使いやすく拡張する

iOSのUIKitにあるUIAlertView、UIActionSheetは非常に使い勝手が悪いと思います。
問題としてはControllerで複数使う場合にdelegateメソッドが被ってしまうことと、ボタン分岐がindex管理されている点にあると思います。
標準のクラスをそのまま使うとControllerのインスタンス変数が溢れかえったり、ボタンを可変にする場合などindexを可変にする必要が出てくるためにかなり煩雑なコードになります。

UIAlertViewとUIActionSheetをblocksで使いやすくするのエントリーでblocksを利用してUIAlertView、UIActionSheetを拡張して使いやすくする方法が紹介されていましたが、blocksはiOS4以降の機構ですので、iOS3でも対応できる形式で書いてみました。


MRAlertView.h

#import <UIKit/UIKit.h>

@interface MRAlertView : UIAlertView <UIAlertViewDelegate> {
  @private
    NSMutableArray *_delegates;
    NSMutableArray *_selectors;
    NSMutableArray *_userInfos;
}
- (id)initWithTitle:(NSString *)title
            message:(NSString *)message
  cancelButtonTitle:(NSString *)cancelButtonTitle;

- (void)addButtonWithTitle:(NSString *)title delegate:(id)delegate selector:(SEL)selector userInfo:(id)userInfo;
- (void)setCancelButtonDelegate:(id)delegate selector:(SEL)selector userInfo:(id)userInfo;

@end

MRAlertView.m

#import "MRAlertView.h"

@interface MRAlertView ()
- (BOOL)hasCancelButton;
@end

@implementation MRAlertView

- (id)initWithTitle:(NSString *)title
            message:(NSString *)message
  cancelButtonTitle:(NSString *)cancelButtonTitle {
    self = [super initWithTitle:title message:message delegate:nil cancelButtonTitle:cancelButtonTitle otherButtonTitles:nil];
    if (self) {
        self.delegate = self;
        _delegates = [[NSMutableArray alloc] init];
        _selectors = [[NSMutableArray alloc] init];
        _userInfos = [[NSMutableArray alloc] init];
        if ([self hasCancelButton]) {
            [self setCancelButtonDelegate:nil selector:nil userInfo:nil];
        }
    }
    return self;
}

- (void)dealloc {
    [_delegates release];
    [_selectors release];
    [_userInfos release];
    [super dealloc];
}


- (void)addButtonWithTitle:(NSString *)title delegate:(id)delegate selector:(SEL)selector userInfo:(id)userInfo {
    [self addButtonWithTitle:title];
    [_delegates addObject:(delegate?delegate:[NSNull null])];
    [_selectors addObject:[NSValue valueWithBytes:&selector objCType:@encode(SEL)]];
    [_userInfos addObject:(userInfo?userInfo:[NSNull null])];
}

- (void)setCancelButtonDelegate:(id)delegate selector:(SEL)selector userInfo:(id)userInfo {
    if ([self hasCancelButton]) {
        if ([_delegates count] > 0) {
            [_delegates removeObjectAtIndex:0];
            [_selectors removeObjectAtIndex:0];
            [_userInfos removeObjectAtIndex:0];
        }
        [_delegates insertObject:(delegate?delegate:[NSNull null]) atIndex:0];
        [_selectors insertObject:[NSValue valueWithBytes:&selector objCType:@encode(SEL)] atIndex:0];
        [_userInfos insertObject:(userInfo?userInfo:[NSNull null]) atIndex:0];
    }
}

- (BOOL)hasCancelButton {
    return (self.cancelButtonIndex == 0);
}

#pragma mark - UIAlertViewDelegate

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    
    id delegate = [_delegates objectAtIndex:buttonIndex];
    SEL selector;
    [[_selectors objectAtIndex:buttonIndex] getValue:&selector];
    id userInfo = [_userInfos objectAtIndex:buttonIndex];

    if ((NSNull *)delegate != [NSNull null] && (NSNull *)selector != [NSNull null]) {
        if ([delegate respondsToSelector:selector]) {
            [delegate performSelector:selector withObject:((NSNull *)userInfo!=[NSNull null]?userInfo:nil)];
        }
    }
}

@end

MRActionSheet.h

#import <UIKit/UIKit.h>

@interface MRActionSheet : UIActionSheet <UIActionSheetDelegate> {
  @private
    NSMutableArray *_delegates;
    NSMutableArray *_selectors;
    NSMutableArray *_userInfos;
}
- (id)initWithTitle:(NSString *)title;
- (void)addButtonWithTitle:(NSString*)title delegate:(id)delegate selector:(SEL)selector userInfo:(id)userInfo;
- (void)addCancelButtonWithTitle:(NSString *)title delegate:(id)delegate selector:(SEL)selector userInfo:(id)userInfo;
- (void)addDestructiveButtonWithTitle:(NSString *)title delegate:(id)delegate selector:(SEL)selector userInfo:(id)userInfo;

@end


MRActionSheet.m

#import "MRActionSheet.h"

@implementation MRActionSheet

- (id)initWithTitle:(NSString *)title {
    self = [super initWithTitle:title delegate:nil cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
    if (self) {
        self.delegate = self;
        _delegates = [[NSMutableArray alloc] init];
        _selectors = [[NSMutableArray alloc] init];
        _userInfos = [[NSMutableArray alloc] init];
    }
    return self;
}

- (void)dealloc {
    [_delegates release];
    [_selectors release];
    [_userInfos release];
    [super dealloc];
}


- (void)addButtonWithTitle:(NSString*)title delegate:(id)delegate selector:(SEL)selector userInfo:(id)userInfo {
    [self addButtonWithTitle:title];
    [_delegates addObject:(delegate?delegate:[NSNull null])];
    [_selectors addObject:[NSValue valueWithBytes:&selector objCType:@encode(SEL)]];
    [_userInfos addObject:(userInfo?userInfo:[NSNull null])];
}

- (void)addCancelButtonWithTitle:(NSString *)title delegate:(id)delegate selector:(SEL)selector userInfo:(id)userInfo {
    NSUInteger index = [_delegates count];
    [self addButtonWithTitle:title delegate:delegate selector:selector userInfo:userInfo];
    self.cancelButtonIndex = index;
}

- (void)addDestructiveButtonWithTitle:(NSString *)title delegate:(id)delegate selector:(SEL)selector userInfo:(id)userInfo {
    NSUInteger index = [_delegates count];
    [self addButtonWithTitle:title delegate:delegate selector:selector userInfo:userInfo];
    self.destructiveButtonIndex = index;
}


#pragma mark - UIActionSheetDelegate

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
    id delegate = [_delegates objectAtIndex:buttonIndex];
    SEL selector;
    [[_selectors objectAtIndex:buttonIndex] getValue:&selector];
    id userInfo = [_userInfos objectAtIndex:buttonIndex];
    
    if ((NSNull *)delegate != [NSNull null] && (NSNull *)selector != [NSNull null]) {
        if ([delegate respondsToSelector:selector]) {
            [delegate performSelector:selector withObject:((NSNull *)userInfo!=[NSNull null]?userInfo:nil)];
        }
    }
}

@end

使い方はこのようになります。

- (void)showAlertView:(id)sender {
    MRAlertView *alertView = [[[MRAlertViewalloc] initWithTitle:@"Alert Title" 
                                                         message:@"Alert message." 
                                               cancelButtonTitle:@"Cancel"] autorelease];
    [alertView addButtonWithTitle:@"Button1" delegate:self selector:@selector(button1:) userInfo:@"userInfo1"];
    [alertView addButtonWithTitle:@"Button2" delegate:self selector:@selector(button2:) userInfo:@"userInfo2"];
    [alertView setCancelButtonDelegate:self selector:@selector(cancel:) userInfo:@"cancel"];
    [alertView show];
}

- (void)showActionSheet:(id)sender {
    MRActionSheet *actionSheet = [[[MRActionSheetalloc] initWithTitle:@"Sheet Title"] autorelease];
    [actionSheet addButtonWithTitle:@"Button1" delegate:self selector:@selector(button1:) userInfo:@"userInfo1"];
    [actionSheet addButtonWithTitle:@"Button2" delegate:self selector:@selector(button2:) userInfo:@"userInfo2"];
    [actionSheet addDestructiveButtonWithTitle:@"Destruct" delegate:self selector:@selector(destruct:) userInfo:@"destruct"];
    [actionSheet addCancelButtonWithTitle:@"Cancel" delegate:nil selector:niluserInfo:nil];
    [actionSheet showInView:self.view];
}

- (void)button1:(id)userInfo {
    NSLog(@"button1:%@", userInfo);
}
- (void)button2:(id)userInfo {
    NSLog(@"button2:%@", userInfo);
}
- (void)destruct:(id)userInfo {
    NSLog(@"destruct:%@", userInfo);
}
- (void)cancel:(id)userInfo {
    NSLog(@"cancel:%@", userInfo);
}


何かしらのアクションを起こすだけならDelegateで受けるUIAlertView,UIActionSheetやindexは不要なので引き渡さない実装としました。このラップによりindexを管理する必要がなくなりコードが簡素になります。
このコールバックさせる仕組みは標準機能としてラップして欲しいところなんですけどね。