Block を使ってインスタンス毎にメソッド追加/メソッド上書きができるようになる REResponder という NSObject の拡張を作ったので紹介します。REResponder は NSObject 拡張コレクション REKit の一部で、MIT ライセンスの下、GitHub にて公開しています。
REResponder の基本
動的メソッド実装
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 以外のインスタンスには影響を及ぼしません。
REResponder の活用例
REResponder を使うと何が嬉しいのか? 以下、GitHub 上の日本語 README で紹介している活用例のコードを転載します。面白そうだと思ったら、日本語 README 全文を読んでみてください。REResponder の機能、挙動、活用例の説明がもう少し詳しく書かれています。
それ自身をデリゲートにする
↓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 のデリゲートメソッドが呼ばれるかを、mock オブジェクトを用意することでテストしています。
__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;
}];
}