インスタンス毎の動的メソッド実装/上書き機能を備えた REKit
2012年7月12日から、GitHub にて REKit (リキット) というプロジェクトを進めていましたが、公開できるレベルになったと思うので、ここで紹介します。
* 2013.02.12 時点での情報になります。最新情報は、REKit on GitHub や日本語 README を参照してください。
REKit は、iOS, OS X の開発で使える NSObject の拡張コレクションです。現時点では Blocks の潜在能力を引き出すような 2つ の機能を提供しています:
- REResponder: Block を使ったインスタンスの動的メソッド実装/上書き機能
- REObserver: Block を使って KVO (Key-Value Observing) を実現する機能 + α
本命は REResponder です。REResponder は、インスタンス (クラスではなくインスタンス) にメソッドを追加したり、メソッドを上書きする機能を提供します。おわり。ただそれだけなのですが、iOS, OS X の開発に大きな変化をもたらすと考えています。僕自身、SpliTron の開発から REKit (当時なので REKit の前身) を使用していますが、もう REKit なしの開発は考えられないという存在になっています。以下、REResponder の肝だけ紹介して、その後に、少々強引ではありますが日本語 README で紹介した REResponder の活用例のコードを転載します。もし面白そうだと思ったら、日本語 README 全文を読んでみてください。機能、挙動、活用例の説明がもう少し詳しく書かれています。
REKit が多くのプロジェクトに採用され、iOS, OS X の世界に寄与することを願っています。
動的メソッド実装
NSObject には -sayHello というメソッドはありませんが、以下のようにするとインスタンス obj に -sayHello メソッドを追加することができます。
id obj; obj = [[NSObject alloc] init]; [obj respondsToSelector:@selector(sayHello) withKey:nil usingBlock:^(id receiver) { NSLog(@"Hello World!"); }]; [obj performSelector:@selector(sayHello)]; // Hello World!
obj 以外のインスタンスには影響を及ぼしません。
動的メソッド上書き
-sayHello メソッドで “No!” をログる MyObject クラスのインスタンス obj があったとします。以下のようにすると “Hello World!” をログるように上書きすることができます。動的メソッド実装のときと同じ -respondsToSelector:withKey:usingBlock: を使用します。
MyObject *obj; obj = [[MyObject alloc] init]; // [obj sayHello]; // No! [obj respondsToSelector:@selector(sayHello) withKey:nil usingBlock:^(id receiver) { NSLog(@"Hello World!"); }]; [obj sayHello]; // Hello World!
こちらも、obj 以外のインスタンスには影響を及ぼしません。
活用例
それ自身をデリゲートにする
UIAlertView のデリゲートに UIAlertView 自身を設定しています。デリゲートメソッドが呼ばれたときの処理を続けて書けるなど、便利です。
UIAlertView *alertView; alertView = [[UIAlertView alloc] initWithTitle:@"title" message:@"message" delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil ]; [alertView respondsToSelector:@selector(alertView:didDismissWithButtonIndex:) withKey:nil usingBlock:^(id receiver, UIAlertView *alertView, NSInteger buttonIndex) { // Do something… } ]; alertView.delegate = alertView;
それ自身をターゲットにする
UIButton のターゲットを UIButton 自身に設定しています。UIButton が沢山ある状況でアクションメソッドが呼ばれとき、まずはどのボタンが押されたのか調べる‥‥その手間がなくなります。
UIButton *button; // … [button respondsToSelector:@selector(buttonAction) withKey:@"key" usingBlock:^(id receiver) { // Do something… }]; [button addTarget:button action:@selector(buttonAction) forControlEvents:UIControlEventTouchUpInside]; [cell.contentView addSubview:button];
UnitTest で、モックオブジェクトを用意する
BalloonController のデリゲートメソッドが呼ばれるかどうかを、モックオブジェクトを用意することでテストしています。
__block BOOL called = NO; // Make mock id mock; mock = [[NSObject alloc] init]; [mock respondsToSelector:@selector(balloonControllerDidDismissBalloon:) withKey:nil usingBlock:^(id receiver, BalloonController *balloonController) { called = YES; } ]; balloonController.delegate = mock; // Dismiss balloon [balloonController dismissBalloonAnimated:NO]; STAssertTrue(called, @"");
UnitTest で、ハイコストな処理をスタブ化する
AccountManager の画像ダウンロード処理をスタブ化して、accountViewController のテストをしています。
// Load sample image __weak UIImage *sampleImage; NSString *sampleImagePath; sampleImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"sample" ofType:@"png"]; sampleImage = [UIImage imageWithContentsOfFile:sampleImagePath]; // Stub out download process [[AccountManager sharedManager] respondsToSelector:@selector(downloadProfileImageWithCompletion:) withKey:@"key" usingBlock:^(id receiver, void (^completion)(UIImage*, NSError*)) { // Execute completion block with sampleImage completion(sampleImage, nil); // Remove current block [receiver removeCurrentBlock]; } ]; // Call thumbnailButtonAction which causes download of profile image [acccountViewController thumbnailButtonAction]; STAssertEqualObjects(accountViewController.profileImageView.image, sampleImage, @"");
関心/機能をまとめる
ノーティフィケーションの監視開始/終了コードを、普通ならファイル中いろいろなところに散在してしまうところ、-_manageKeyboardWillShowNotification メソッドにまとめています。
- (id)initWithCoder:(NSCoder *)aDecoder { // super self = [super initWithCoder:aDecoder]; if (!self) { return nil; } // Manage _keyboardWillShowNotificationObserver [self _manageKeyboardWillShowNotificationObserver]; return self; } - (void)_manageKeyboardWillShowNotificationObserver { __block id observer; observer = _keyboardWillShowNotificationObserver; #pragma mark └ [self viewWillAppear:] [self respondsToSelector:@selector(viewWillAppear:) withKey:nil usingBlock:^(id receiver, BOOL animated) { // supermethod REVoidIMP supermethod; // REVoidIMP is defined like this: typedef void (*REVoidIMP)(id, SEL, ...); if ((supermethod = (REVoidIMP)[receiver supermethodOfCurrentBlock])) { supermethod(receiver, @selector(viewWillAppear:), animated); } // Start observing if (!observer) { observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { // Do something… } ]; } }]; #pragma mark └ [self viewDidDisappear:] [self respondsToSelector:@selector(viewDidDisappear:) withKey:nil usingBlock:^(id receiver, BOOL animated) { // supermethod REVoidIMP supermethod; if ((supermethod = (REVoidIMP)[receiver supermethodOfCurrentBlock])) { supermethod(receiver, @selector(viewDidDisappear:), animated); } // Stop observing [[NSNotificationCenter defaultCenter] removeObserver:observer]; observer = nil; }]; }
Trackbacks & Pingbacks