-[NSObject performSelector:withObject]に複数の引数を渡す
イントロダクション
あるメソッドを遅延して実行したい場合、-[NSObject performSelector:withObject:afterDelay]を用いることが多々ある。しかし対象のメソッドが複数の引数を持つものであった場合、この方法は直接的には用いることができなくなってしまう。そこでNSInvocationクラスとNSProxyクラスを用いて複数引数を持つメソッドでも適用できるように工夫する。なおこの方法はHigher Order Messaging(メッセージを引数に持つメッセージという意味合い)と呼ばれる。
*******************************************************************
HOMについてまとめた記事はこちら。
Higher Order Messaging (HOM) – boreal-kiss.com
*******************************************************************
問題の一例
例えば以下のようなコントローラークラスを考える。このコントローラーではUI中のボタン(Show Alert)が押されるとアラートが表示される仕組みになっている。
@implementation AppController -(IBAction)buttonPressed:(id)sender{ [self openAlertWithTitle:@"Alert" message:@"This is an alert." defaultButtonTitle:@"OK"]; } -(void)openAlertWithTitle:(NSString *)aTitle message:(NSString *)aMessage defaultButtonTitle:(NSString *)aDefaultButtonTitle{ NSRunAlertPanel(aTitle, aMessage, aDefaultButtonTitle, nil, nil); } @end
このままだと、ボタンが押された途端にアラートが表示され、ボタンの描画状態がプッシュ中のままになることがわかる。気持ちわるい。
そこで-[NSObject performSelector:withObject:afterDelay:]を用いてアラートを遅延表示させるのが得策だろう。しかし-[NSObject performSelector:withObject:afterDelay:]では-[AppController showAlertWithTitle:message:defaultButtonTitle:]を正しく実行できない。引数が複数あるためだ。それではどうすればよいだろうか。解決策のひとつとして複数の引数をひとつにまとめてしまう方法が考えられる。
NSInvocation
NSInvocationはメソッドをオブジェクト化したクラスである。デザインパターンで言うところのCommandパターンに相当するもので、これを利用することで上記問題を解決することができる。具体的には以下のような手順を踏むことになるだろう。
- -[NSObject performSelector:withObject:afterDelay:]で本来実行したい複数引数を持つメソッドをNSInvocationオブジェクトとしてひとつにまとめてしまう(メソッドのオブジェクト化)
- 作成したNSInvocationオブジェクトを実行するための新たなメソッド(引数はNSInvocationオブジェクトひとつのみ)を定義、それを-[NSObject performSelector:withObject:afterDelay:]で実行する。単一引数なので問題なし
- NSInvocationオブジェクトの中身(本来実行したいメソッド)を実行する
以下では新しく-[AppController openDelayedAlertWithTitle:message:defaultButtonTitle:]としてアラートの遅延表示を実現している。
@implementation AppController -(IBAction)buttonPressed:(id)sender{ [self openDelayedAlertWithTitle:@"Alert" message:@"This is an alert." defaultButtonTitle:@"OK"]; } -(void)openAlertWithTitle:(NSString *)aTitle message:(NSString *)aMessage defaultButtonTitle:(NSString *)aDefaultButtonTitle{ NSRunAlertPanel(aTitle, aMessage, aDefaultButtonTitle, nil, nil); } //New -(void)openDelayedAlertWithTitle:(NSString *)aTitle message:(NSString *)aMessage defaultButtonTitle:(NSString *)aDefaultButtonTitle{ NSMethodSignature *aSignature = [[self class] instanceMethodSignatureForSelector: @selector(openAlertWithTitle:message:defaultButtonTitle:)]; NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:aSignature]; [anInvocation setTarget:self]; [anInvocation setArgument:&aTitle atIndex:2]; [anInvocation setArgument:&aMessage atIndex:3]; [anInvocation setArgument:&aDefaultButtonTitle atIndex:4]; [anInvocation setSelector: @selector(openAlertWithTitle:message:defaultButtonTitle:)]; [self performSelector:@selector(performInvocation:) withObject:anInvocation afterDelay:0.0]; } //New -(void)performInvocation:(NSInvocation *)anInvocation{ [anInvocation invokeWithTarget:self]; } @end
実行結果。アラート表示が遅延されボタンがプッシュ状態でなくなっているのがわかる。気持ちいい。
上記例からメソッドをオブジェクト化するというNSInvocationクラスの有用性が実感できた。反面、NSInvocationオブジェクトは作成するまでの手順が面倒なこともわかったと思う(例えば引数が10個あるメソッドをNSInvocationオブジェクト化するためには10回も-[NSInvocation setArgument:atIndex:]を書かないといけない)。実は適当なメッセージを送るとメッセージ内容をNSInvocationオブジェクトにしてくれる便利クラスが存在する。NSProxyだ。
NSProxy
NSProxyはデザインパターンで言うところのProxyパターンを実現するものである。NSProxyは自分が理解できないメッセージをとりあえずNSInvocationオブジェクトにしてしまうという機能を持っているのでこれを利用する。NSProxyはメッセージを受け取ると以下のような手順でNSInvocationオブジェクトを作成し、NSInvocationオブジェクトをどのように扱うのか指示を待つ。
- -[NSProxy methodSignatureForSelector:]を呼び出しNSMthodSignatureオブジェクトを作成する。ただしNSMethodSignatureオブジェクトを作成するための必要な情報(ターゲット etc.)はメソッド内に適切に与えてやる必要がある
- NSMethodSignatureオブジェクトを元にNSInvocationオブジェクトを作成する
- -[NSProxy forwardInvocation:]を呼び出しメソッド内容の指示に従う
自動作成されたNSInvocationオブジェクトは-[NSProxy forwardInvocation:]の引数に渡されるので、あとはこのNSInvocationオブジェクトの扱い方を内部に記述してやればよい。以下ではNSProxyのサブクラスとしてTrampolineクラスを用いてNSInvocationオブジェクトを作成させている。AppControllerの中身の変更にも注目してもらいたい。
#import "Trampoline.h" @implementation AppController -(IBAction)buttonPressed:(id)sender{ [self openDelayedAlertWithTitle:@"Alert" message:@"This is an alert." defaultButtonTitle:@"OK"]; } -(void)openAlertWithTitle:(NSString *)aTitle message:(NSString *)aMessage defaultButtonTitle:(NSString *)aDefaultButtonTitle{ NSRunAlertPanel(aTitle, aMessage, aDefaultButtonTitle, nil, nil); } //New -(void)openDelayedAlertWithTitle:(NSString *)aTitle message:(NSString *)aMessage defaultButtonTitle:(NSString *)aDefaultButtonTitle{ [[self trampoline] openAlertWithTitle:aTitle message:aMessage defaultButtonTitle:aDefaultButtonTitle]; } -(void)performInvocation:(NSInvocation *)anInvocation{ [anInvocation invokeWithTarget:self]; } //New -(Trampoline *)trampoline{ return [Trampoline trampolineWithTarget:self selector:@selector(performInvocation:)]; } @end
@interface Trampoline : NSProxy { id _target; SEL _selector; } -(id)initWithtarget:(id)aTarget selector:(SEL)aSelector; +(id)trampolineWithTarget:(id)aTarget selector:(SEL)aSelector; @end
@implementation Trampoline -(id)initWithtarget:(id)aTarget selector:(SEL)aSelector{ _target = aTarget; _selector = aSelector; return self; } +(id)trampolineWithTarget:(id)aTarget selector:(SEL)aSelector{ return [[[[self class] alloc] initWithtarget:aTarget selector:aSelector] autorelease]; } //Override -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ return [_target methodSignatureForSelector:aSelector]; } //Override -(void)forwardInvocation:(NSInvocation *)anInvocation{ [_target performSelector:_selector withObject:anInvocation afterDelay:0.0]; } -(void)dealloc{ _target = nil; [super dealloc]; } @end
[…] -[NSObject performSelector:withObject]に複数の引数を渡す – boreal-kiss.comで少しだけ言及していたHigher Order Messaging (HOM)の例をいくつか挙げておく。なおCocoaにおけるHOMはMetaobjectによるMPWFoundationの機能の一部として利用することができる(ようである)。 […]
[…] Footnotesメッセージフォワーディングの具体例は-[NSObject performSelector:withObject]に複数の引数を渡す – boreal-kiss.com [↩] […]