20
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Block を使ってインスタンス毎にメソッド追加/上書きができるようになる REResponder

Last updated at Posted at 2013-02-12

 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;
   }];
}

リンク

REKit
REKit 日本語 README

20
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?