日々精進

新しく学んだことを書き留めていきます

iOSアプリでフレームワーククラスのメソッドをフックする方法

Method Swizzlingとは「クラスのメソッドテーブルを書き換えることでセレクタ名のリネーミングを行なう技法」。
要はフレームワークメソッドを自前のメソッドでオーバーライドする方法。
objective-cではカテゴリを使えばオーバーライドできそうだが、正しく動作しない場合がある。
理由はフレームワークのクラスもカテゴリを使って実装されていることがあり、複数のカテゴリで同じメソッドが定義されている場合どちらを優先するかは決まっていないため。
正しくはJRSwizzleを使って行う。例は以下。


AppDelegate.mで以下のようにオーバーライドするメソッドとされるメソッドを指定する。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
    〜
    NSError *error = nil;
    [UIView jr_swizzleMethod:@selector(addSubview:)
                  withMethod:@selector(myAddSubview:)
                       error:&error];
    〜
}

オーバーライドしたいメソッドを持つクラスのカテゴリを作成する。その中でオーバーライドメソッドを実装する。

#import "UIView.h"

@implementation UIView (CustomMethods)
- (void)myAddSubview:(UIView *)view {
    float version = [[[UIDevice currentDevice] systemVersion] floatValue];
    if (version < 5) {
        // iOS5未満の場合、フレームワークがaddSubView前にviewWillAppearとviewDidAppearを呼んでくれないためここで呼ぶ
        UIViewController *ownerViewController = (UIViewController *)[view traverseResponderChainForUIViewController];
        if (ownerViewController){
            [ownerViewController viewWillAppear:YES];
            [ownerViewController viewDidAppear:YES];
        }
    }
    // ここでmyAddSubviewを呼ぶと実行時はオリジナルのメソッド(addSubview)を呼ぶことになる
    [self myAddSubview:view];
}
- (id) traverseResponderChainForUIViewController {
    id nextResponder = [self nextResponder];
    if ([nextResponder isKindOfClass:[UIViewController class]]) {
        return nextResponder;
    } else if ([nextResponder isKindOfClass:[UIView class]]) {
        return [nextResponder traverseResponderChainForUIViewController];
    } else {
        return nil;
    }
}
@end

上記の例ではaddSubViewした時の動作がiOSのバージョンによって違うという問題を解決しています。