いろいろな UIAlertView
iPhone でユーザー通知や警告などに使われる UIAlertView
について代表的なカスタマイズ方法をまとめてみる。
- 2013/10/28 追記
- この記事で紹介している
UIAlertView - addSubView
を利用したカスタマイズは、iOS 7 以降では利用できません - そのため代替案を検討する記事を書きました。
- iOS 7 以降の UIAlertView カスタマイズ代替について考える
- この記事で紹介している
セレクターをボタン毎に設定する
UIAlertView
の内容やボタンが複数あるときに標準の UIAlertViewDelegate - clickedButtonAtIndex
でハンドリングするのは非常に面倒だ。内容を判定するには UIAlertView
の tag
プロパティを利用するかオーナーとなるクラス側に状態を持つことになる。押されたボタンについてはインデックスしか情報がないため可変長のときに困る。
tag
に設定する値をビットフラグにすれば組み合わせの複雑さも多少は緩和できるだろう。願わくば直感的に、例えばボタンとそのハンドラを一対一で結びつけられないものか。というわけで UIAlertView
とデリゲート関連でググっていたら以下の記事を見つけた。
これらの記事では UIAlertView
派生クラスを実装して UIAlertViewDelegate
と blocks
や selector
を結びつけるアイディアを提示している。記事を読むに前者の blocks
案が元でそれを iOS 3 で動くように selector
へ置き換えたのが後者のようだが、そちらの方が好みである。
ただし多くの用途では一つの delegate
に複数の selector
という設計になると思うので、それを前提に以下のようなクラスを作成してみた。
#import <UIKit/UIKit.h>
/**
* UIAlertView を拡張したコントロールです。
*/
@interface CustomAlertView : UIAlertView<UIAlertViewDelegate>
+ (CustomAlertView*)alertWithButton:(NSString*)title message:(NSString*)message delegate:(id)delegate button:(NSString*)button;
- (void)addButtonWithTitle:(NSString *)title selector:(SEL)selector userInfo:(id)userInfo;
- (void)close:(NSInteger)buttonIndex;
- (void)setFirstButtonDelegate:(SEL)selector userInfo:(id)userInfo;
@end
ひとつの delegate
を持ち addButtonWithTitle
や setFirstButtonDelegate
で selector
を追加してゆく。close
は好みのインデックスを指定してアラートを閉じる dismissWithClickedButtonIndex
の Wrapper である。
alertWithButton
に対応する initWithButton
は非公開とした。コンビニエンス メソッドだけ公開というのは一般的ではない気がするがどうなのだろう?個人的に同じような目的を持つ方法が複数あるのは好ましくなく、また autorelease
を強制するという意味でこのようなケースならありだと思うのだが。もしこのあたりに意見をお持ちの方がいらっしゃったら意見をいただけるとありがたいです。
クラス実装は以下。
#import "CustomAlertView.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - CustomAlertEvent
/**
* CustomAlertView 用のイベント用データを表します。
*/
@interface CustomAlertEvent : NSObject
@property (nonatomic, assign) SEL selector; //! コールバック メソッド
@property (nonatomic, assign) id userInfo; //! コールバックメソッドに指定されるパラメータ
- (id)initWithSelector:(SEL)aSelector userInfo:(id)aUserInfo;
@end
@implementation CustomAlertEvent
@synthesize selector, userInfo;
/**
* インスタンスを初期化します。
*
* @param aSelector セレクター。
* @param aUserInfo ユーザー データ。
*
* @return インスタンス。
*/
- (id)initWithSelector:(SEL)aSelector userInfo:(id)aUserInfo
{
self = [super init];
if( self )
{
self.selector = aSelector;
self.userInfo = aUserInfo;
}
return self;
}
/**
* メモリを解放します。
*/
-(void)dealloc
{
self.selector = nil;
self.userInfo = nil;
[super dealloc];
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - CustomAlertView
@interface CustomAlertView()
@property (nonatomic, assign) id myDelegate; //! 利用者
@property (nonatomic, retain) NSMutableArray* events; //! イベント情報のコレクション
- (id)initWithButton:(NSString*)title message:(NSString*)message delegate:(id)aDelegate button:(NSString*)button;
- (BOOL)hasCancelButton;
@end
@implementation CustomAlertView
@synthesize myDelegate, events, indicator, progress, table, textField;
#pragma mark - Lifecycle
/**
* インスタンスを初期化します。
*
* @param title タイトル。
* @param message 本文。
* @param aDelegate デリゲート。
* @param button ボタンの文言。
*
* @return インスタンス。
*/
- (id)initWithButton:(NSString *)title message:(NSString *)message delegate:(id)aDelegate button:(NSString *)button
{
self = [super initWithTitle:title message:message delegate:nil cancelButtonTitle:button otherButtonTitles:nil];
if( self )
{
self.delegate = self;
self.myDelegate = aDelegate;
self.events = [[[NSMutableArray alloc] init] autorelease];
if( [self hasCancelButton] )
{
[self setFirstButtonDelegate:nil userInfo:nil];
}
}
return self;
}
/**
* メモリを解放します。
*/
- (void)dealloc
{
self.events = nil;
[super dealloc];
}
/**
* ボタンを追加します。
*
* @param title ボタンのタイトル。
* @param selector コールバック メソッド
* @param userInfo コールバック メソッドに指定されるパラメータ。
*/
- (void)addButtonWithTitle:(NSString *)title selector:(SEL)selector userInfo:(id)userInfo
{
[self addButtonWithTitle:title];
CustomAlertEvent* event = [[CustomAlertEvent alloc] initWithSelector:selector userInfo:userInfo];
[self.events addObject:event];
[event release];
}
/**
* アラートを閉じます。
*
* @param buttonIndex 閉じる時に押されたボタンのインデックス。ボタンの無いアラートの場合は 0 を指定します。
*/
- (void)close:(NSInteger)buttonIndex
{
[self dismissWithClickedButtonIndex:buttonIndex animated:YES];
}
/**
* はじめのボタンが押された時のハンドラを設定します。
*
* @param selector コールバック メソッド
* @param userInfo コールバック メソッドに指定されるパラメータ。
*/
- (void)setFirstButtonDelegate:(SEL)selector userInfo:(id)userInfo
{
if( [self hasCancelButton] )
{
if( self.events.count > 0 )
{
[self.events removeObjectAtIndex:0];
}
CustomAlertEvent* event = [[CustomAlertEvent alloc] initWithSelector:selector userInfo:userInfo];
[self.events insertObject:event atIndex:0];
[event release];
}
}
selector
やパラメータは CustomAlertEvent
というファイルスコープのクラスを実装して、そのコレクションを管理するように設計。なお CustomAlertEvent
の userInfo
は assign
になっているため寿命は保証しない。
以降のカスタマイズはこのクラスをベースにおこなってゆく。
単純なボタン
まずは基本の標準的なボタン付きアラート。
見た目は標準のものと一緒だが前述の拡張によりボタンが押された時のハンドラが selector
になっている。使い方は以下のような感じ。
/**
* CustomAlertView 上のボタンが押された時に発生します。
*
* @param userInfo ユーザー情報。
*/
- (void)selectAlertNormalButton:(id)userInfo
{
if( userInfo )
{
NSString* text = ( NSString* )userInfo;
CustomAlertView* alert = [CustomAlertView alertWithButton:nil message:text delegate:nil button:@"OK"];
[alert show];
}
}
/**
* ボタン付きアラートを表示します。
*/
- (void)showAlertButton
{
CustomAlertView* alert = [CustomAlertView alertWithButton:@"タイトル" message:@"メッセージ" delegate:self button:cancel];
[alert addButtonWithTitle:@"OK" selector:@selector( selectAlertNormalButton: ) userInfo:@"OK Button"];
[alert setFirstButtonDelegate:@selector( selectAlertNormalButton: ) userInfo:@"Cancel Button"];
[alert show];
}
ボタンが押された時に selectAlertNormalButton
で更にその種別をアラートで表示するようにしている。userInfo
には静的な文字列を指定しているため寿命については意識する必要がない。一つの selector
で複数ボタンをサポートするならこの使い方が基本になる。
テキスト フィールド
パスワード入力などを求める時に使えそうなテキスト フィールドつきアラート。
テキスト フィールドつきアラートを作成するため CustomAlertView
に以下のコンビニエンス メソッドを追加。
+ (CustomAlertView*)alertWithTextField:(NSString*)title message:(NSString*)message delegate:(id)delegate textDelegate:(id<UITextFieldDelegate>)textDelegate placeholder:(NSString*)placeholder button:(NSString*)button;
テキスト フィールドの実体やデザイン的な部分については非公開とする。最低限、必要であろう UITextFieldDelegate
と placeholder
のみを指定出来るようにした。
@interface CustomAlertView()
@property (nonatomic, retain) UITextField* textField; //! テキスト フィールド
- (id)initWithTextField:(NSString*)title message:(NSString*)message delegate:(id)aDelegate textDelegate:(id<UITextFieldDelegate>)textDelegate placeholder:(NSString*)placeholder button:(NSString*)button;
// ... 中略
/**
* テキスト フィールド付きアラートのインスタンスを初期化します。
*
* @param title タイトル。
* @param aDelegate デリゲート。
* @param textDelegate テキスト フィールドからのメッセージ通知先。
* @param placeholder テキスト フィールドのプレースホルダー文言。
* @param button ボタンの文言。
*
* @return インスタンス。
*/
- (id)initWithTextField:(NSString *)title message:(NSString *)message delegate:(id)aDelegate textDelegate:(id<UITextFieldDelegate>)textDelegate placeholder:(NSString*)placeholder button:(NSString *)button
{
self = [self initWithButton:title message:message delegate:aDelegate button:button];
if( self )
{
self.textField = [[[UITextField alloc] initWithFrame:CGRectMake( 12, 80, 260, 31 )] autorelease];
self.textField.delegate = textDelegate;
self.textField.placeholder = placeholder;
self.textField.borderStyle = UITextBorderStyleRoundedRect;
[self.textField setBackgroundColor:[UIColor whiteColor]];
[self addSubview:self.textField];
}
return self;
}
/**
* UIAlertView が表示される時に発生します。
*
* @param alertView 表示対象となる UIAlertView。
*/
- (void)willPresentAlertView:(UIAlertView *)alertView
{
// ボタンが皆無なら、表示関連の調整は不要
if( self.numberOfButtons < 1 ) { return; }
NSInteger offsetY = 0;
NSInteger offsetH = 0;
if( self.textField )
{
offsetY = self.textField.frame.origin.y;
offsetH = self.textField.frame.size.height;
}
else
{
return;
}
const NSInteger padding = 8;
// アラート本体の位置とサイズを調整。
// 垂直位置を追加したコントロールの半分、ずらすことで中央としている。
// ただし、小数点以下の値が混じると描画がぼやけるため、offsetH を意図的に NSInteger で宣言している。
//
CGRect frame = self.frame;
frame.origin.y -= offsetH / 2;
frame.size.height += offsetH + padding;
self.frame = frame;
// 追加コントロール以下のものを位置調整
for( UIView* view in self.subviews )
{
frame = view.frame;
if( offsetY < frame.origin.y )
{
frame.origin.y += offsetH + padding;
view.frame = frame;
}
}
}
UIAlertView - addSubview
でコントロールを追加すると標準ボタンと重なって表示されてしまう。そのためアラート本体を拡大してボタン位置をずらす必要がある。これは layoutSubview
か UIAlertViewDelegate
を実装したクラスの willPresentAlertView
でおこなう。今回のクラスでは後者を採用。
以降、様々なコントロールを追加してゆくがそれらにも同様の処理が必要。ただし内容は変わりないため処理の解説はこの項にだけ書いておく。
テキスト フィールドの内容を取得したい場合は UITextFieldDelegate - textFieldDidEndEditing
を利用。このアラートを閉じたたらこのハンドラが呼び出されるので、指定された textField
の text
プロパティからテキストが得られる。ちなみにアラートを閉じるとキーボードも連動して閉じられる。
テーブル
拡張性が高く、様々な用途に利用できるテーブル付きアラート。
テーブルのセルや挙動は UITableViewDataSource
と UITableViewDelegate
によって定義できるため、これらを受け取るコンビニエンス メソッドを提供する。
+ (CustomAlertView*)alertWithTable:(NSString*)title message:(NSString*)message delegate:(id)delegate tableSource:(id<UITableViewDataSource>)tableSource tableDelegate:(id<UITableViewDelegate>)tableDelegate button:(NSString*)button;
テーブルのスタイルは UITableViewStylePlain
。そのまま表示すると角張っているため角丸効果を加えている。
@interface CustomAlertView()
@property (nonatomic, retain) UITableView* table; //! テーブル
- (id)initWithTable:(NSString*)title message:(NSString*)message delegate:(id)aDelegate tableSource:(id<UITableViewDataSource>)tableSource tableDelegate:(id<UITableViewDelegate>)tableDelegate button:(NSString*)button;
// ... 中略
/**
* テーブル付きアラートのインスタンスを初期化します。
*
* @param title タイトル。
* @param aDelegate デリゲート。
* @param tableSource テーブルのデータ取得先。
* @param tableDelegate テーブルからのメッセージ通知先。
* @param button ボタンの文言。
*
* @return インスタンス。
*/
- (id)initWithTable:(NSString *)title message:(NSString *)message delegate:(id)aDelegate tableSource:(id<UITableViewDataSource>)tableSource tableDelegate:(id<UITableViewDelegate>)tableDelegate button:(NSString *)button
{
self = [self initWithButton:title message:message delegate:aDelegate button:button];
if( self )
{
self.table = [[[UITableView alloc] initWithFrame:CGRectMake( 12 , 80, 260, 200 ) style:UITableViewStylePlain] autorelease];
self.table.dataSource = tableSource;
self.table.delegate = tableDelegate;
self.table.layer.cornerRadius = 4;
[self addSubview:self.table];
}
return self;
}
UITableView
系デリゲートをハンドリングできるので、かなり柔軟にカスタマイズできるはず。
プログレスバー
処理数が判明しており進捗が取得できる場合に有用なプログレス バー付きアラート。
メッセージは決め打ちで "%d / %d"
としており指定不可。ここには「処理数 / 処理総数」が自動的に割り当てられる。進捗を更新するために updateProgress
メソッドを公開。これを処理の度に呼び出すことでメッセージとプログレスバーが更新される。
+ (CustomAlertView*)alertWithProgress:(NSString*)title delegate:(id)delegate button:(NSString*)button;
- (void)updateProgress:(NSInteger)value total:(NSInteger)total;
実装は以下。
#define PROGRESS_MESSAGE @"%d / %d"
@interface CustomAlertView()
@property (nonatomic, retain) UIProgressView* progress; //! プログレスバー
- (id)initWithProgress:(NSString*)title delegate:(id)aDelegate button:(NSString*)button;
// ... 中略
/**
* プログレスバー付きアラートのインスタンスを初期化します。
*
* @param title タイトル。
* @param aDelegate デリゲート。
* @param button ボタンの文言。
*
* @return インスタンス。
*/
- (id)initWithProgress:(NSString *)title delegate:(id)aDelegate button:(NSString *)button
{
NSString* message = [NSString stringWithFormat:PROGRESS_MESSAGE, 0, 0];
self = [self initWithButton:title message:message delegate:aDelegate button:button];
if( self )
{
self.progress = [[[UIProgressView alloc] initWithFrame:CGRectMake( 12, 80, 260, 30 )] autorelease];
[self addSubview:self.progress];
}
return self;
}
/**
* 進捗を更新します。
*
* @param value 処理数。
* @param total 処理の総数。
*/
- (void)updateProgress:(NSInteger)value total:(NSInteger)total
{
if( self.progress )
{
[self setMessage:[NSString stringWithFormat:PROGRESS_MESSAGE, value, total]];
self.progress.progress = ( float )value / ( float )total;
}
}
インジケーター
時間の掛かる処理があるとしてその開始と終了しか取れない時に利用できそうなインジケーターつきアラート。
このアラートを選ぶならボタンは不要な気がするけれど念のため指定可能としておく。
+ (CustomAlertView*)alertWithIndicator:(NSString*)title message:(NSString*)message delegate:(id)delegate button:(NSString*)button;
実装は以下。
@interface CustomAlertView()
@property (nonatomic, retain) UIActivityIndicatorView* indicator; //! インジケーター
- (id)initWithIndicator:(NSString*)title message:(NSString*)message delegate:(id)aDelegate button:(NSString*)button;
// ... 中略
/**
* インジケーター付きアラートのインスタンスを初期化します。
*
* @param title タイトル。
* @param message 本文。
* @param aDelegate デリゲート。
* @param button ボタンの文言。
*
* @return インスタンス。
*/
- (id)initWithIndicator:(NSString *)title message:(NSString *)message delegate:(id)aDelegate button:(NSString *)button
{
self = [self initWithButton:title message:message delegate:aDelegate button:button];
if( self )
{
self.indicator = [[[UIActivityIndicatorView alloc] initWithFrame:CGRectMake( 125, 80, 30, 30 )] autorelease];
self.indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
[self addSubview:self.indicator];
[self.indicator startAnimating];
}
return self;
}
ボタンを用意しないとユーザー操作でアラートが閉じられない。よってプログラム側で確実に閉じるよう実装すること。
サンプル プログラム
サンプル プログラムのプロジェクト一式を GitHub にて公開した。開発は Xcode 4.5.1、動作確認には iPhone 5.1 シミュレーター、iPod touch 第 5世代 ( 昨日とどいた ) を使用。
ライセンスは The MIT License (MIT)。fork、改変などはご自由にどうぞ。
- !!注意!!
- 以下の旧サンプルではメモリー リークが起きます
- この問題を修正した GitHub 公開版と比較できるようにリンクは残しておきますが、ご利用の際はご注意ください。
- メモリーリークの原因については 調査 #70: UIAlertView サンプルのメモリーリークを調査する - Examples-iOS - Akabeko Projects に書きました
以下はメモリー リークのある旧サンプル
Comments from WordPress
- 藤岡 良英 2012-10-30T10:56:38Z
ありがとうございます。すごく役に立ちそうです。
参考にさせて頂きたいと思います。それで、早速自分のアプリに組み込んで動かそうとしたところ、Instrumentsでメモリーリークが検出されました。
サンプルソフトそのものでも、同様に発生します。私はまだ、初心者のため、原因が分からないのですが、私のツールの使い方が悪いかも知れません。
ちなみにXcodeは、V4.5です。
気にしなくても良いでしょうか?ご教示の程、よろしくお願いします。
- akabeko 2012-10-30T23:03:59Z
ご指摘ありがとうございました。
メモリーリークについて、ちかい内に時間をとって調査しようと思います ( 週末になるかもしれません )。結果については、この記事に追記する予定です。
- akabeko 2012-11-03T14:24:45Z
藤岡さん、こんばんは。
その後、メモリー リークについて調査をおこない、原因が判明したので修正いたしました。修正版は GitHub に公開しています。
あわせて記事内のソースを修正し、サンプル プログラムのコーナーに GitHub へのリンクと注意書きを追加しました。
ご指摘がなければ、問題は放置されたままになっていたと思います。今後も、疑問点などがありましたら、コメントをいただけると助かります。ありがとうございました。