IconUtility - iPhone/iPad用のアイコンイメージ作成アプリケーション
Tags: App, Cocoa, iPad, iPhone, Mac, Objective-C
![]()
画像をドロップするとiPhone/iPad用のアイコン用にリサイズするMac用アプリケーションです。対応サイズと出力ファイル名は以下の通り1。
- Icon.png - 57×57ピクセルのiPhoneアイコン
- Icon@2x.png - 114×114ピクセルのiPhone 4アイコン
- Icon-72.png - 72×72ピクセルのiPadアイコン
- Icon-Small.png - 29×29ピクセルのiPhoneセッティングアイコン
- Icon-Small@2x.png - 58×58ピクセルのiPhone 4セッティングアイコン
- Icon-Small-50.png - 50×50ピクセルのiPadセッティングアイコン
ソースとアプリケーション本体は以下に置いてあります。
Footnotes
Associative Storage: カテゴリに疑似インスタンス変数を追加
Tags: Cocoa, DesignPatterns, Objective-C
イントロダクション
Objective-Cのカテゴリはサブクラスを作ることなくメソッドの拡張(またはメソッドごとの分類)が行える点で強力だ。しかしメソッドの定義はできてもインスタンス変数の追加ができない。そこでCocoaのKey Value Coding (KVC)の機能を用いてカテゴリに擬似的にインスタンス変数を生成させる。これにより元クラスを手直しすることなくインスタンス変数を追加することができる。なおこの手法は、よりフレキシブルという理由でNSMapTableを用いるのが一般的なようだが、ここではNSMutableDictionaryを代用した。理由は使い慣れているからだ。NSMapTableを用いた方法はBuck and Yacktman - Cocoa Design Patternsが詳しい。
具体例
簡単な例としてボタンを押すとアラートを表示するアプリケーションを考える。ボタンプッシュ後の動作はAppControllerクラスで以下のように定義されているとする。
//AppController.m @implementation AppController -(IBAction)buttonPressed:(id)sender{ NSRunAlertPanel(@"Alert", @"This is an alert.", @"OK", nil, nil); } @end
ここで「一度アラート表示された場合は再度アラートを表示させない」仕様に変更するとしよう。AppControllerにBOOL値を格納するインスタンス変数を追加して制御するのが手っ取り早いが、ここではAppControllerのカテゴリ(ShowAtOnce)を新規に定義して制御することにする。カテゴリ作成の大まかな手順は以下のようになる。
- 疑似インスタンス変数格納用にNSMutableDictionary変数をカテゴリに定義
- 疑似インスタンス変数としたいオブジェクトを上記NSMutableDictionary変数に格納。その際のキーストリングはAppControllerインスタンスに固有のものを用いる
- 格納したオブジェクトのAccesors (Setters/Getters)をカテゴリに定義
- インスタンスの解放時にNSMutableDictionaryからオブジェクトを消去するメソッドをカテゴリに定義
以下順にみていこう。
1. 疑似インスタンス変数格納用変数の定義
疑似インスタンス変数にしても情報を格納する場所は必要になる。そこで疑似インスタンス変数を格納する場所としてNSMutableDictionaryオブジェクトをカテゴリ(ShowAtOnce)に定義する。さらにこのオブジェクトのAccesorsをクラスメソッドとして定義する。つまりこの格納用のNSMutableDictionaryオブジェクトはAppControllerインスタンスすべてで使い回されることになる。
//AppController-ShowAtOnce.m @implementation AppController (ShowAtOnce) static NSMutableDictionary *_simulatedIVars = nil; +(NSMutableDictionary *)_simulatedIVars{ if (_simulatedIVars == nil){ _simulatedIVars = [[NSMutableDictionary alloc] init]; } return _simulatedIVars; } @end
2. インスタンス固有のキーストリング
上記例のアプリケーションの仕様を実現するためには、一度アラートが表示されたかどうかを調べるBOOL値を(NSMutableDictionary *)_simulatedIVarsに格納できればよさそうだ。しかし今は疑似インスタンス変数を作りたいのでAppControllerインスタンスごとに異なる値を格納できるようにする必要がある。そこでまず「AppControllerインスタンスに固有なNSStringオブジェクト」を生成するメソッドを定義する。このメソッドは-[AppController self]のストリング表現を返す。
//AppController-ShowAtOnce.m -(NSString *)_instanceID{ return [NSString stringWithFormat:@"%@", self]; }
さらに「疑似インスタンス変数名に相当するNSStringオブジェクト」を用意する。これはクラス全体で使い回しができるようにすればよい。ここではAlertHasEverBeenShownKeyStringという名前で定義しておこう。
//AppController-ShowAtOnce.m static NSString * const AlertHasEverBeenShownKeyString = @"alertHasEverBeenShown";
あとは「AppControllerインスタンスに固有なNSStringオブジェクト」と「疑似インスタンス変数名に相当するNSStringオブジェクト」を適当に繋げて「あるAppControllerインスタンスの疑似インスタンス変数に固有なNSStringオブジェクト」を作成するメソッドを定義する。このメソッドの返り値が(NSMutableDictionary *)_simulatedIVarsへの格納用キーストリングとして利用されるわけだ。
//AppController-ShowAtOnce.m -(NSString *)_objectKeyForKeyString:(NSString *)aKeyString{ return [NSString stringWithFormat:@"%@:%@", [self _instanceID], aKeyString]; }
現在までのAppController (ShowAtOnce)の中身をまとめると以下のようになっているはずだ。
//AppController-ShowAtOnce.m static NSString * const AlertHasEverBeenShownKeyString = @"alertHasEverBeenShown"; @implementation AppController (ShowAtOnce) static NSMutableDictionary *_simulatedIVars = nil; +(NSMutableDictionary *)_simulatedIVars{ if (_simulatedIVars == nil){ _simulatedIVars = [[NSMutableDictionary alloc] init]; } return _simulatedIVars; } #pragma mark - #pragma mark Utilities -(NSString *)_instanceID{ return [NSString stringWithFormat:@"%@", self]; } -(NSString *)_objectKeyForKeyString:(NSString *)aKeyString{ return [NSString stringWithFormat:@"%@:%@", [self _instanceID], aKeyString]; } @end
3. 疑似インスタンス変数のAccesors
上記メソッド群を利用してカテゴリ(ShowAtOnce)に定義する。Settersは-[AppController _setAlertHasEverBeenShown:]、Gettersは-[AppController _alertHasEverBeenShown]としよう。ここはstraightforwardだと思う。
//AppController-ShowAtOnce.m -(BOOL)_alertHasEverBeenShown{ NSString *aKey = [self _objectKeyForKeyString:AlertHasEverBeenShownKeyString]; id object = [[[self class] _simulatedIVars] objectForKey:aKey]; if (object == nil){ return NO; } return [object boolValue]; } -(void)_setAlertHasEverBeenShown:(BOOL)yn{ NSString *aKey = [self _objectKeyForKeyString:AlertHasEverBeenShownKeyString]; [[[self class] _simulatedIVars] setObject:[NSNumber numberWithBool:yn] forKey:aKey]; }
あとはAppController内でこのメソッドを使ってやればよい。具体的には以下のようになるだろう。
//AppController.m #import "AppController-ShowAtOnce.h" @implementation AppController -(IBAction)buttonPressed:(id)sender{ if (![self _alertHasEverBeenShown]){ NSRunAlertPanel(@"Alert", @"This is an alert.", @"OK", nil, nil); [self _setAlertHasEverBeenShown:YES]; } } @end
4. 疑似インスタンス変数を解放
AppControllerインスタンスごとに(NSMutableDictionary *)_simulatedIVarsに登録されるのだからAppControllerインスタンスが不要になったら疑似インスタンス変数も解放する必要がある。そのために以下のようなメソッドをAppController(ShowAtOnce)に定義しておく。このメソッドは呼ばれれば不要になった疑似インスタンス変数を解放する。
//AppController-ShowAtOnce.m -(void)_removeSimulatedIVars{ NSString *aKey = [self _objectKeyForKeyString:AlertHasEverBeenShownKeyString]; id object = [[[self class] _simulatedIVars] objectForKey:aKey]; if (object){ [[[self class] _simulatedIVars] removeObjectForKey:aKey]; } }
さてこれまでAppControllerそのものを一切変更しないように実装すると言ってきたが実は嘘で、AppControllerそのものを手直しする必要がでてくる。というのも疑似インスタンス変数の解放を呼ぶメソッドをカテゴリに書くことは危険だからだ。例えば-[AppController dealloc]をカテゴリ(ShowAtOnce)に再定義することは可能だが、もし他に疑似インスタンス変数を持つ別カテゴリが同様の-[AppController dealloc]を実装した場合どちらの-[AppController dealloc]が呼び出されるかわからなくなるからだ(結果どちらかのカテゴリの疑似インスタンス変数は解放メソッドを呼ばれないがために生き残ってしまう)。したがって疑似インスタンス変数の解放メソッドを呼び出すのはAppControllerとするのがよいだろう。
//AppController.m -(void)dealloc{ [self _removeSimulatedIVars]; [super dealloc]; }
全ソースコード
最終的なコードは以下のようになっている。
//AppController.h #import <Cocoa/Cocoa.h> @interface AppController : NSObject { } -(IBAction)buttonPressed:(id)sender; @end
//AppController.m #import "AppController.h" #import "AppController-ShowAtOnce.h" @implementation AppController -(IBAction)buttonPressed:(id)sender{ if (![self _alertHasEverBeenShown]){ NSRunAlertPanel(@"Alert", @"This is an alert.", @"OK", nil, nil); [self _setAlertHasEverBeenShown:YES]; } } -(void)dealloc{ [self _removeSimulatedIVars]; [super dealloc]; } @end
//AppController-ShowAtOnce.h #import <Cocoa/Cocoa.h> #import "AppController.h" @interface AppController (ShowAtOnce) +(NSMutableDictionary *)_simulatedIVars; -(void)_removeSimulatedIVars; -(BOOL)_alertHasEverBeenShown; -(void)_setAlertHasEverBeenShown:(BOOL)yn; -(NSString *)_instanceID; -(NSString *)_objectKeyForKeyString:(NSString *)aKeyString; @end
//AppController-ShowAtOnce.m #import "AppController-ShowAtOnce.h" //Private static NSString * const AlertHasEverBeenShownKeyString = @"alertHasEverBeenShown"; @implementation AppController (ShowAtOnce) static NSMutableDictionary *_simulatedIVars = nil; +(NSMutableDictionary *)_simulatedIVars{ if (_simulatedIVars == nil){ _simulatedIVars = [[NSMutableDictionary alloc] init]; } return _simulatedIVars; } -(void)_removeSimulatedIVars{ NSLog(@"%s", __FUNCTION__); NSString *aKey = [self _objectKeyForKeyString:AlertHasEverBeenShownKeyString]; id object = [[[self class] _simulatedIVars] objectForKey:aKey]; if (object){ [[[self class] _simulatedIVars] removeObjectForKey:aKey]; } } #pragma mark - #pragma mark Setters Getters -(BOOL)_alertHasEverBeenShown{ NSString *aKey = [self _objectKeyForKeyString:AlertHasEverBeenShownKeyString]; id object = [[[self class] _simulatedIVars] objectForKey:aKey]; if (object == nil){ return NO; } return [object boolValue]; } -(void)_setAlertHasEverBeenShown:(BOOL)yn{ NSString *aKey = [self _objectKeyForKeyString:AlertHasEverBeenShownKeyString]; [[[self class] _simulatedIVars] setObject:[NSNumber numberWithBool:yn] forKey:aKey]; } #pragma mark - #pragma mark Utilities -(NSString *)_instanceID{ return [NSString stringWithFormat:@"%@", self]; } -(NSString *)_objectKeyForKeyString:(NSString *)aKeyString{ return [NSString stringWithFormat:@"%@:%@", [self _instanceID], aKeyString]; } @end
Higher Order Messaging (HOM)
Tags: Cocoa, NSInvocation, NSProxy, Objective-C
イントロダクション
-[NSObject performSelector:withObject]に複数の引数を渡す - boreal-kiss.comで少しだけ言及していたHigher Order Messaging (HOM)の例をいくつか挙げておく。なおCocoaにおけるHOMはMetaobjectによるMPWFoundationの機能の一部として利用することができる(ようである)。
ケース1
ボタンを押す(-[AppController buttonPressed:])とアラートを表示する(-[AppController openAlert])アプリケーションを考えよう。アプリケーションのコントローラー部分は以下のように記述されているとする。
@implementation AppController -(IBAction)buttonPressed:(id)sender{ [self openAlert]; } -(void)openAlert{ NSRunAlertPanel(@"Alert", @"This is an alert", @"OK", nil, nil); } @end
1秒後にアラート表示をさせたい場合は-[NSObject performSelector:withObject:afterDelay:]を用いると以下のように書き換えることができる。
-(IBAction)buttonPressed:(id)sender{ [self performSelector:@selector(openAlert) withObject:nil afterDelay:1.0]; }
この書き方でももちろんかまわないが、HOMを用いるとメッセージ内容がより直感的に表現される。新しくNSProxyのサブクラス(DelayedTrampoline)を導入してHOM的な書き方をした場合は以下のようになる。
#import "DelayedTrampoline.h" @implementation AppController //New -(IBAction)buttonPressed:(id)sender{ [[self afterDelay:1.0] openAlert]; } -(void)openAlert{ NSRunAlertPanel(@"Alert", @"This is an alert", @"OK", nil, nil); } //New -(DelayedTrampoline *)afterDelay:(NSTimeInterval)delay{ return [DelayedTrampoline delayedTrampolineWithTarget:self afterDelay:delay]; } @end
@interface DelayedTrampoline : NSProxy { id _target; NSTimeInterval _delay; } -(id)initWithTarget:(id)aTarget afterDelay:(NSTimeInterval)delay; +(id)delayedTrampolineWithTarget:(id)aTarget afterDelay:(NSTimeInterval)delay; @end
@implementation DelayedTrampoline -(id)initWithTarget:(id)aTarget afterDelay:(NSTimeInterval)delay{ _target = aTarget; _delay = delay; return self; } +(id)delayedTrampolineWithTarget:(id)aTarget afterDelay:(NSTimeInterval)delay{ return [[[[self class] alloc] initWithTarget:aTarget afterDelay:delay] autorelease]; } //Override -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ return [_target methodSignatureForSelector:aSelector]; } //Override -(void)forwardInvocation:(NSInvocation *)anInvocation{ [anInvocation performSelector:@selector(invokeWithTarget:) withObject:_target afterDelay:_delay]; } -(void)dealloc{ _target = nil; [super dealloc]; } @end
以下に比較したい箇所だけを抜き出す。HOMの場合メッセージ(afterDelay:)の引数がメッセージ(openAlert)であるかのように書くことができることがわかる。これがHOMの”Higher Order”たる所以である。[NSObject performSelector:withObject:afterDelay:]を用いた従来の書き方よりもより直感的に理解できる表現だ。もちろん引数に書かれたメッセージは実際に引数を持つことができるため柔軟性も高い(例: -[NSObject performSelector:withObject]に複数の引数を渡す - boreal-kiss.com)。
//Normal -(IBAction)buttonPressed:(id)sender{ [self performSelector:@selector(openAlert) withObject:nil afterDelay:1.0]; } //HOM -(IBAction)buttonPressed:(id)sender{ [[self afterDelay:1.0] openAlert]; }
ケース2
HOMは同じ内容を繰り返し記述しなければならない場合などにも便利だ。例えばdelegateメソッドを実行する際にターゲットであるdelegateが実際にメソッドを実装しているか確認する次のようなコードを大量に書いて(あるいはコピペして)骨が折れた経験はないだろうか。
if ([_delegate respondsToSelector:@selector(doSomething)]){ [_delegate doSomething]; }
この場合あらかじめdelegateオブジェクトを扱うクラス(例えばAppController)にHOMを実装しておけば問題は回避可能だ。
@interface AppController : NSObject { id _delegate; } @end
#import "IfRespondsToTrampoline.h" @implementation AppController -(IfRespondsToTrampoline *)delegateIfRespondsTo{ if (_delegate){ return [IfRespondsToTrampoline ifRespondsToTrampolineWithTarget:_delegate]; } return nil; } @end
@interface IfRespondsToTrampoline : NSProxy { id _target; } -(id)initWithTarget:(id)aTarget; +(id)ifRespondsToTrampolineWithTarget:(id)aTarget; @end
@implementation IfRespondsToTrampoline -(id)initWithTarget:(id)aTarget{ _target = aTarget; return self; } +(id)ifRespondsToTrampolineWithTarget:(id)aTarget{ return [[[[self class] alloc] initWithTarget:aTarget] autorelease]; } //Override -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ return [_target methodSignatureForSelector:aSelector]; } //Override -(void)forwardInvocation:(NSInvocation *)anInvocation{ [anInvocation invokeWithTarget:_target]; } -(void)dealloc{ _target = nil; [super dealloc]; } @end
以下に比較したい箇所だけ抜粋する。AppControllerに上記のようなHOMが実装されていれば、delegateオブジェクトがメソッドを実装していれば実行、なければ何もしない(実際には内部でNSProxyによるNSInvalidArgumentExceptionが挙がる)というコードが一行で書けてしまう。
//In AppController.m //Normal if ([_delegate respondsToSelector:@selector(doSomething)]){ [_delegate doSomething]; } //HOM [[self delegateIfRespondsTo] doSomething];
-[NSObject performSelector:withObject]に複数の引数を渡す
Tags: Cocoa, DesignPatterns, NSInvocation, NSProxy, Objective-C
イントロダクション
あるメソッドを遅延して実行したい場合、-[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
References
-[NSObject performSelector:]の返り値
Tags: Cocoa, iPhone, Objective-C
まとめ
- -[NSObject performSelector:]の返り値として不適切なものが存在する。
- それら不適切なものを扱う場合の対処法について。
イントロダクション
NSObjectのインスタンスメソッド
- (void)performSelector:(SEL) aSelector
はvoid型となっているが、「引数にセットされたメソッドの返り値」を自分の返り値に持つことができる。ただしその場合、-[NSObject performSelector:]の返り値はid型(オブジェクトポインタ)になるので適切にキャストしてやる必要がある。例えば、以下のようなNSStringを返すメソッドを持つTest1クラスを考える。
@implementation Test1 -(NSString *)stringValue{ return @"Hello"; } @end
このTest1クラスに-[NSObject performSelector:]を使って-[Test1 stringValue]を呼び、NSStringにキャストして返り値を受け取るとたしかに@”Hello”が返ってきていることがわかる。
Test1 *test1 = [[Test1 alloc] init]; NSString *stringValue = (NSString *)[test1 performSelector:@selector(stringValue)]; NSLog(@"%@", stringValue);// Hello
本題
-[NSObject performSelector:]の返り値はid型であり、id型はオブジェクトへのポインタであると説明した。つまり、-[NSObject performSelector:]は「オブジェクトではないものを返り値とするメソッド」の返り値を受け取ることができない可能性があることがわかる。例を見てみよう。Test2クラスに以下のようなメソッドがあるとする。どのメソッドの返り値もオブジェクトではない。
@implementation Test2 -(BOOL)boolValue{ return YES; } -(int)intValue{ return 10; } -(float)floatValue{ return 2.0; } -(CGPoint)point{ return CGPointMake(100.0, 100.0); } @end
先ほどと同様にこれらのメソッドを-[NSObject performSelector:]を使って呼び出し、返り値を確認すると以下のような結果になった。
Test2 *test2 = [[Test2 alloc] init]; //warning: cast from pointer to integer of different size BOOL boolValue = (BOOL)[test2 performSelector:@selector(boolValue)]; NSLog(@"%i", boolValue);// 1 int intValue = (int)[test2 performSelector:@selector(intValue)]; NSLog(@"%i", intValue);// 10 //error: pointer value used where a floating point value was expected float floatValue = (float)[test2 performSelector:@selector(floatValue)]; NSLog(@"%f", floatValue); //error: conversion to non-scalar type requested CGPoint point = (CGPoint)[test2 performSelector:@selector(point)]; NSLog(@"%@", NSStringFromCGPoint(point));
結果をまとめると以下のようになる。
- 返り値BOOLについては警告が出たが返り値の受け取りができた。
- 返り値intについては警告は出ず返り値の受け取りができた。
- 返り値floatについてはコンパイルエラーが生じ実行できなかった。
- 返り値CGPointについてはコンパイルエラーが生じ実行できなかった。
対処法
上記のテスト結果から、-[NSObject performSelector:]から「オブジェクトではないものを返り値とするメソッド」の返り値を受け取るためには何かしらの工夫をしなければならないことがわかった(int型・BOOL型などの例外もある)。以下に二種類の対処法を挙げておく。
- -[NSObject performSelector:]で呼び出すメソッドの返り値をオブジェクトにしておく。
- -[NSObject performSelector:]の代わりに[NSInvocation invoke]を用いる。
順に見てみよう。
対処法1
二つ目の対処法より簡単である。返り値int, BOOL, floatについてはNSNumberオブジェクトによるラッピング、返り値CGPoint(CGRect, CGSizeなども含む)についてはNSStringオブジェクトに変換することで簡単に対処できる。以下に、Test2クラスの4メソッドの返り値を全てオブジェクトに変更したTest3クラスを挙げる。
@implementation Test3 -(NSNumber *)boolValue{ return [NSNumber numberWithBool:YES]; } -(NSNumber *)intValue{ return [NSNumber numberWithInt:10]; } -(NSNumber *)floatValue{ return [NSNumber numberWithFloat:2.0]; } -(NSString *)point{ return NSStringFromCGPoint(CGPointMake(100.0, 100.0)); } @end
この場合、各メソッドの返り値はオブジェクトであるため、それらオブジェクトから必要な値を復元してやればよい。
Test3 *test3 = [[Test3 alloc] init]; BOOL boolValue = [[test3 performSelector:@selector(boolValue)] boolValue]; NSLog(@"%i", boolValue2);// 1 int intValue = [[test3 performSelector:@selector(intValue)] intValue]; NSLog(@"%i", intValue2);// 10 float floatValue = [[test3 performSelector:@selector(floatValue)] floatValue]; NSLog(@"%f", floatValue2);// 2.000000 CGPoint point = CGPointFromString([test3 performSelector:@selector(point)]); NSLog(@"%@", NSStringFromCGPoint(point2));// {100, 100}
対処法2
-[NSObject performSelector:]の代わりにNSInvocationを用いると、返り値がオブジェクトであろうとなかろうと受け取ることが可能になる。例えば、-[Test2 floatValue]の返り値を受け取る場合には、以下のような手続きをとる。
Test2 *test2 = [[Test2 alloc] init]; float floatValue; NSMethodSignature *aSigniture = [[test2 class] instanceMethodSignatureForSelector:@selector(floatValue)]; NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:aSigniture]; [anInvocation setSelector:@selector(floatValue)]; [anInvocation setTarget:test2]; [anInvocation invoke]; [anInvocation getReturnValue:&floatValue]; NSLog(@"%f", floatValue);// 2.000000
NSInvocationを用いると汎用性は高くなるが、一般的にソースが煩雑になる。