状況:
- 画面が10個ほどあるiPhoneアプリ
- UIViewControllerのviewWillAppear等にログを仕込みたい。
- すでにUIViewControllerを継承したクラスが多数あって、全部にログを仕込むのはめんどくさいしダサくていやだ。
解決策:
- method_exchangeImplementationsでメソッドを入れ替えてしまえばいいよ!
黒魔法へようこそ
Objective-C Runtime には、method_exchangeImplementations()という関数が用意されています。これで「メソッドの交換」が可能となります。これはSDKで提供されているクラスのメソッドも入れ替える事が可能です。なのでUIViewControllerのviewWillAppear等を自作メソッドと差し替える事が出来ます。この差し替えは、動的に行われるので、動作する環境に応じて入れ替えをコントロールしたり、元に戻したりといった事が可能となります。
この方法は、"Method Swizzling"と呼ばれるものみたいです。
以下のような実装をします。
- カテゴリ拡張で、入れ替えるメソッドを用意します。
- このとき、「元々実装していたメソッド名」と「入れ替えた実装のメソッド名」も入れ替わるので、無限ループしそうなソースコードになります。
- class_getInstanceMethod()で、メソッドへの参照(Method)を取得します。
- method_exchangeImplementations()で、メソッドを入れ替えます。
サンプルコード
サンプルコードでは、UIViewControllerのviewDidLoad
、viewWillAppear
、viewDidAppear
、viewWillDisappear
、viewDidDisappear
を、NSLog出力付きの自作メソッドに入れ替えています。
まずは、UIViewControllerをカテゴリ拡張して、入れ替えるメソッドの実装と、method_exchangeImplementations()を呼び出すクラスメソッドの実装を行います。
#import <UIKit/UIKit.h>
@interface UIViewController(MethodSwitch)
+(void)switchLoggingMethod;
@end
#import "UIViewController+MethodSwitch.h"
#import <objc/runtime.h>
@implementation UIViewController(MethodSwitch)
// 入れ替えるメソッドを準備
-(void)loggingViewDidLoad
{
NSLog(@"%@ viewDidLoad",NSStringFromClass(self.class));
[self loggingViewDidLoad]; // ここ、無限ループしそうだけど、メソッドの実装が入れ替わるので、元々あったviewDidLoadの実装が呼ばれます。
}
-(void)loggingViewWillAppear:(BOOL)animated{
NSLog(@"%@ viewWillAppear",NSStringFromClass(self.class));
[self loggingViewWillAppear:animated];
}
-(void)loggingViewDidAppear:(BOOL)animated{
NSLog(@"%@ viewDidAppear",NSStringFromClass(self.class));
[self loggingViewDidAppear:animated];
}
-(void)loggingViewWillDisappear:(BOOL)animated{
NSLog(@"%@ viewWillDisappear",NSStringFromClass(self.class));
[self loggingViewWillDisappear:animated];
}
-(void)loggingViewDidDisappear:(BOOL)animated{
NSLog(@"%@ viewDidDisappear",NSStringFromClass(self.class));
[self loggingViewDidDisappear:animated];
}
+(void)switchLoggingMethod
{
// メソッドを入れ替える
[self switchInstanceMethodFrom:@selector(viewDidLoad) To:@selector(loggingViewDidLoad) ];
[self switchInstanceMethodFrom:@selector(viewWillAppear:) To:@selector(loggingViewWillAppear:) ];
[self switchInstanceMethodFrom:@selector(viewDidAppear:) To:@selector(loggingViewDidAppear:) ];
[self switchInstanceMethodFrom:@selector(viewWillDisappear:) To:@selector(loggingViewWillDisappear:)];
[self switchInstanceMethodFrom:@selector(viewDidDisappear:) To:@selector(loggingViewDidDisappear:) ];
}
+(void)switchInstanceMethodFrom:(SEL)from To:(SEL)to
{
// メソッドの入れ替えの実態はここ
Method fromMethod = class_getInstanceMethod(self,from);
Method toMethod = class_getInstanceMethod(self,to );
method_exchangeImplementations(fromMethod, toMethod);
}
@end
また、AppDelegate内で、[UIViewController switchLoggingMethod]を呼び出します。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
[UIViewController switchLoggingMethod]; // ←これでメソッドが入れ替わる
return YES;
}
UIViewControllerそのものでも有効になりますし、それを継承したクラス(UINavigationController等)でも有効になります。
用量用法を守って、正しくお使いください。
このメソッドの入れ替えですが、普通にコードを読んで入れ替わってる事に気づくのは難しいです。知らない人がソースコードを読むと、「viewDidLoadに何も実装していないのにログが出力されるナンデ?!」と大混乱する事必至です。チームメンバーや未来の自分を混乱させないように、この辺りの情報はがっつり残しておきましょう。
でも、悪戯って楽しいよね。
例えば NSObject のメソッドを入れ替えちゃったりして。うへへへ
参考にしたサイト
Safari用独自プラグインを作る(4) - Method Swizzling を試す
method_exchangeImplementationsでクラスメソッドを置換する
☆ モテる Objective-C 女子力を磨くための4つの心得「method swizzling できない女をアピールせよ」等 ←タイトルに負けた