目的
モーダルビューを表示し、設定された内容を呼び出し元のビューに反映させる。
主要クラス
UIViewController
モーダルビュー呼び出し側のUIViewControllerは、モーダルビューが画面から消えるまでユーザーとの会話はできない。
そのためモーダルビューがユーザーによって設定されて画面から消えるのか、キャンセルされて消えるのかを知る方法が必要になる。
この方法として、iPhoneアプリではCocoaでの常套パターンであるDelegateパターンを利用することが推奨されている。
実際、システムが提供する画像選択用コントローラであるUIViewControllerのUIImagePickerControllerやアドレス帳を参照するためのコントローラABPeoplePickerNavigationControllerでは設定されたかキャンセルされたかを知るためにDelegateパターンが利用されている。
それにならい、このドリルでもDelegateパターンを利用する。
Delegateパターンとは
Appleのドキュメントではデザインパターンで言うところのDecoratorパターンであり、Template Methodパターンの一部でもあるとの説明がされている。後述のサンプル実装説明で具体的な例を示す。
Appleのドキュメント:Cocoa Fundamentals Guide
Cocoaにおけるデザインパターンの応用の章を参照。
Appleのドキュメントではデザインパターンで言うところのDecoratorパターンであり、Template Methodパターンの一部でもあるとの説明がされている。後述のサンプル実装説明で具体的な例を示す。
Appleのドキュメント:Cocoa Fundamentals Guide
Cocoaにおけるデザインパターンの応用の章を参照。
使用テンプレートプロジェクト
Window-based Application
プロジェクトの名称
CustomViewController
サンプル実装説明
「モーダルビューを表示する(1)」で作成したカスタムUIViewControllerのビューを"緑"、"灰色"、"キャンセル"の3つのボタンを持つビューに修正する。
アプリ起動と同時にモーダルビューが表示され、上記3つのボタンのいずれかを押すとモーダルビューは消えて呼び出し元のビューが表示される。
この時、"緑"ボタンが押されていたらビューの背景を緑に、"灰色"なら灰色に変更する。"キャンセル"なら変更しない。
「モーダルビューを表示する(1)」ではモーダルビューを閉じるためにparentViewControllerにdismissModalViewControllerAnimated:メッセージを送っていた。
今回は、この部分を自分で定義した独自メッセージを送るように変更する。
独自メッセージは以下のように選択されたボタンに対応するUIColorのインスタンスをパラメータinColorに持つ。inColorがnilだった場合をキャンセルと考える。
-(void)selectColor:(UIColor*)inColor;
このメッセージを送る相手をid変数として用意。
@interface CustomViewController : UIViewController {
id colorSelectDelegate;
}
id colorSelectDelegate;
}
同時にこの変数を設定するメソッドも用意する。
-(void)setColorSelectDelegate:(id)inDelegate {
colorSelectDelegate = inDelegate;
}
colorSelectDelegate = inDelegate;
}
CustomViewControllerのviewDidLoadは3つのラウンドレクトボタンを表示するようにし、それぞれのボタンインスタンスのtagプロパティに0、1、2を割り付ける。
"緑"ボタンに1、"灰色"ボタンに2、"キャンセル"ボタンに0を割り当てた。
ボタンが押された時に呼び出されるアクションメソッドでは以下のようにボタンのtag情報を元に色を決定しcolorSelectDelegateに上記独自メッセージを送る。
-(void)dismiss:(id)inSender {
UIView* view = (UIView*)inSender;
UIColor* requestColor = nil;
if (view.tag == 1)
requestColor = [UIColor greenColor];
if (view.tag == 2)
requestColor = [UIColor grayColor];
[colorSelectDelegate selectColor:requestColor];
}
UIView* view = (UIView*)inSender;
UIColor* requestColor = nil;
if (view.tag == 1)
requestColor = [UIColor greenColor];
if (view.tag == 2)
requestColor = [UIColor grayColor];
[colorSelectDelegate selectColor:requestColor];
}
inSenderのtagプロパティからパラメータに使うrequestColorの値を設定している。
UIButton*ではなくUIView*にキャストしている理由はtagプロパティがUIViewクラスで定義しているためUIButtonクラスを必要としないから。
このようにしておくと、このメソッドはUIButton以外でも利用できる。もちろんUIButton*しか取り扱う気がない事を表明するためにUIButton*にキャストする記述でもかまわない。
どのみち送られたきたidがUIButtonやUIViewクラスである保証はなにもない。そこまでチェックしたい場合は
とする。
このようにしておくと、このメソッドはUIButton以外でも利用できる。もちろんUIButton*しか取り扱う気がない事を表明するためにUIButton*にキャストする記述でもかまわない。
どのみち送られたきたidがUIButtonやUIViewクラスである保証はなにもない。そこまでチェックしたい場合は
if ([inSender isKindOfClass:[UIView class]])
...
...
とする。
これを受け取る側は以下のようにinColorパラメータの色によって背景を決め、モーダルビューを消す作業をおこなっている。
-(void)selectColor:(UIColor*)inColor {
if (inColor != nil)
self.view.backgroundColor = inColor;
[self dismissModalViewControllerAnimated:YES];
}
if (inColor != nil)
self.view.backgroundColor = inColor;
[self dismissModalViewControllerAnimated:YES];
}
あとはpresentModalViewControllerを呼び出す前にcontrollerBにsetColorSelectDelegateでselectColorメッセージを投げる相手を設定。
[controllerB setColorSelectDelegate:controller];
ビルドして実行すると、起動直後に赤色背景のモーダルビューに切り替わり"緑"ボタンを押すと、元々の通常ビューが、青色背景から緑色背景になって表示される。"灰色"ボタンなら"灰色"、"キャンセル"ボタンなら青色背景のまま表示される。
このような形をとることで、ボタンを押された後の動作を、送り先の相手に委譲すなわちDelegateでき、送られたUIColorを背景色に使用するのも、何かの表示用の文字色に使うのも送られた側の自由となる。
すなわちモードルビュー側の変更無しにボタン押し下げ時の動作を拡張できるということであり、これがAppleがDelegateをDecoratorパターンであると説明している理由となる。
プロジェクト
検討
このままでは、以下のようなワーニングが出る。
「UIButtonをプログラム上で作り利用する」では以下の部分
でid変数を送り先にするとワーニングが出ないと考えたが、送るメッセージがCocoaフレームワークのいずれのクラスにも存在しない名称の場合はワーニングが表示されるらしい。
[inSender setTitle:[NSString
stringWithFormat:@"count:%d", count]
forState:UIControlStateNormal];
stringWithFormat:@"count:%d", count]
forState:UIControlStateNormal];
でid変数を送り先にするとワーニングが出ないと考えたが、送るメッセージがCocoaフレームワークのいずれのクラスにも存在しない名称の場合はワーニングが表示されるらしい。
しかし独自定義なので、このようにワーニングが出る方が適切といえる。
では
-(void)selectColor:(UIColor*)inColor {
を送り先がメッセージとして受け付けることをどう表明すればいいか?
このためにObjective-Cのプロトコル宣言
@protocol ColorSelectDelegate
-(void)selectColor:(UIColor*)inColor;
@end
-(void)selectColor:(UIColor*)inColor;
@end
と、その継承
@interface CustomViewController : UIViewController<ColorSelectDelegate>
が利用される。
プロトコル宣言(@protocol)はインターフェース宣言(@interface)と違いは、変数を持てず、定義したメソッドを必ずしも実装しなくてもよく、複数の同時継承が可能な点にある。
例),で区切って続ける事で、クラスClassDはClassAと共にProtocolB, ProtocolCのプロトコルを同時に継承できる。
例),で区切って続ける事で、クラスClassDはClassAと共にProtocolB, ProtocolCのプロトコルを同時に継承できる。
@interface ClassD : ClassA<ProtocolB, ProtocolC>
これがAppleがDelegateをTemplate Methodパターンの一部であると説明している理由。
あとはid型を以下のようにColorSelectDelegateプロトコルを継承していると宣言してやればよい。
id<ColorSelectDelegate> colorSelectDelegate; // 委譲用インスタンス
ただし、これだけではメッセージを送った相手が実際には上記メソッドを実装していない場合、ハングアップしてしまうので、その対応も必要。
送り先が指定したメッセージに対応可能かは以下のように、送り先にrespondsToSelectorメッセージを送ってYESを返すかどうかで判断できる。
if ([colorSelectDelegate respondsToSelector:@selector(selectColor:)])
[colorSelectDelegate selectColor:requestColor];
[colorSelectDelegate selectColor:requestColor];
最後に、このrespondsToSelector:メッセージはNSObjectが持っているものなので、ColorSelectDelegateプロトコル宣言を若干修正しなければ、この変更自体でワーニンが出てしまう。
@protocol ColorSelectDelegate<NSObject>
colorSelectDelegateの宣言を
id<ColorSelectDelegate> colorSelectDelegate
から
id<ColorSelectDelegate, NSObject> colorSelectDelegate
↑Appleのヘッダーでは@protocol ColorSelectDelegate<NSObject>という宣言だったので、これに従う事にした。
としないと、この変更自体でワーニンが出てしまう。
これで最初の独自メソッドselectColor:はColorSelectDelegateプロトコルで宣言された正式なものでありid<ColorSelectDelegate , NSObject> colorSelectDelegateは、そのメソッドおよびrespondsToSelectorメソッドを正しく継承しているインスタンスと認められワーニングがおさまる。
この一連の作業がDelegateパータンの実装ということになる。
プロジェクト