Skip to content

インスタンス毎の動的メソッド実装/上書き機能を備えた REKit

2013.02.12

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

リンク

REKit
REKit 日本語 README

From → Develop

Leave a comment