bricklifeのはてなブログ

気軽にブログ書くよ!

iOS 5専用のメソッドをiOS 4でも使えるようにするもうちょっといい方法

ないのなら作ってしまえpresentingViewController - Objective-Cで動的メソッド追加 - bricklifeのはてなブログ」というエントリーで、iOS 5から UIViewController に追加された presentingViewController というメソッド(プロパティ)の疑似メソッドを作成し、iOS 4でも使えるようにするという話をしました。

そこではメソッドが使われる前、例えばアプリケーション起動時などにObjective-Cランタイムの関数を使って追加する方法をとりましたが、今回は NSObject の resolveInstanceMethod: というクラスメソッドを使ってみます。

resolveInstanceMethod:

resolveInstanceMethod: というメソッドは、オブジェクトに送られたメッセージが実装されていないときに呼ばれるクラスメソッドです。Appleのドキュメントにもあるように、自分が作成したクラスで @dynamic なプロパティを作るときなどにオーバーライドして使います。なので、前回のように既存クラスである UIViewController に存在しないメソッドを動的に追加するのには適さないと思っていました。

でも、よく考えたらObjective-Cのカテゴリはその仕組み上、まったく新しいメソッドの追加だけでなくスーパークラスのメソッドをオーバーライドするのにも使えるはずで、UIViewController のカテゴリとして resolveInstanceMethod: を実装したらうまく動きそうです。

カテゴリでメソッドを上書きしてしまわないか

ただ、もしすでに UIViewController に resolveInstanceMethod: が実装されていた場合、カテゴリで同名メソッドを実装したことにより上書き問題が発生するので、結局 method_exchangeImplementations() で乗っ取りしたりしないといけません。というわけで、まずは UIViewController に実装されているクラスメソッドを以下のようなコードで調べてみました。

Class cls = [UIViewController class];
unsigned int count;
Method *methods = class_copyMethodList(object_getClass(cls), &count);
for (int i = 0; i < count; i++) {
    NSLog(@"%d:\t%@", i, NSStringFromSelector(method_getName(methods[i])));
}
free(methods);

class_copyMethodList() はあるクラスに実装されているインスタンスメソッド一覧を取得する関数です。スーパークラスのメソッドは含まれないので、今回のような調査に使えます。また、クラスとして [UIViewController class] をさらに object_getClass() したものを渡すことで、クラスメソッドの一覧を取得することができます。

で、実行してみた結果には resolveInstanceMethod: は見つかりませんでした。同じようにルートクラスである NSObject を調べると当然のことながら見つかるのですが、UIViewController のスーパークラスをさかのぼって調べてみると、結局 NSObject までどのクラスにも実装されてないようです。UIViewController の使い方を考えると、まあ当然の結果かも知れません。

とにかく、これで安心してカテゴリによるオーバーライドをすることができそうです。

実験

まずは実験。UIViewController に iOS5 というカテゴリを追加して、そこに resolveInstanceMethod: だけを実装してみます。

@implementation UIViewController (iOS5)

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    NSLog(@"resolving '%@'", NSStringFromSelector(aSEL));
    return [super resolveInstanceMethod:aSEL];
}

@end

これを UIViewController+iOS5.m という名前で保存してプロジェクトに追加。適当な場所で presentingViewController を使ってみます。

NSLog(@"%@", self.presentingViewController);

iOS 5でこのコードを実行したログは以下。presentingViewController はiOS 5では実装されているので、当然のことながら resolveInstanceMethod: は呼ばれていません。

2012-03-19 12:50:20.775 xxx[21025:207] <ViewController: 0x6833cc0>

次にiOS 4で実行してみます。

2012-03-19 12:50:45.968 xxx[21061:207] resolving 'presentingViewController'
2012-03-19 12:50:45.971 xxx[21061:207] -[ViewController presentingViewController]: unrecognized selector sent to instance 0x5924770
いつものエラーが続く...

おお、ちゃんと resolveInstanceMethod: が呼ばれてる! resolveInstanceMethod: の中身をまったく実装していないので結局エラーになっていますが、これをAppleのドキュメントのように presentingViewController が呼ばれたときに class_addMethod() すればちゃんと動くようになるはず…。

本番

UIViewController+iOS5.m に前回作ったように presentingViewController を偽装する関数を追加して、resolveInstanceMethod: 内で class_addMethod() します。今回は他のメソッドの定義を横取りせずに、一から作成してみました*1

#import <objc/runtime.h>

UIViewController* iOS5_presentingViewController(UIViewController* self, SEL _cmd);

UIViewController* iOS5_presentingViewController(UIViewController* self, SEL _cmd)
{
    if (self.navigationController) {
        return nil;
    }
    return self.parentViewController;
}

@implementation UIViewController (iOS5)

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    NSLog(@"resolving '%@'", NSStringFromSelector(aSEL));

    if (aSEL == @selector(presentingViewController)) {
        Class cls = [self class];
        IMP imp = (IMP)iOS5_presentingViewController;
        const char *types = "@@:";
        
        class_addMethod(cls, aSEL, imp, types);
        return YES;
    }

    return [super resolveInstanceMethod:aSEL];
}

@end

この状態でドキドキしながらさっきのコードを実行してみると…

2012-03-19 12:52:17.889 xxx[21189:207] resolving 'presentingViewController'
2012-03-19 12:52:17.892 xxx[21189:207] <ViewController: 0x5d33f10>

ちゃんと resolveInstanceMethod: が呼ばれた後に presentingViewController を使えるようになってます! Objective-CかわいいよObjective-C。

メリット

この方法のメリットは、なんといってもIOS 5用に書いたコードには一切手を加えず、さっき作った UIViewController+iOS5.m というファイルをプロジェクトに追加するだけでiOS 4でも動くようになるところです。iOS 4対応が必要なくなったら外せばいいだけ。なんといっても、前回のコードとは違って真の意味での動的メソッド追加になっているので気分がいいですw

デメリットは、メソッド呼び出しのオーバーヘッドが増える(と思われる)のと、将来 UIViewController で resolveInstanceMethod: が実装されたときに困ることくらいでしょうか。

まとめ

カテゴリによるメソッドオーバーライドで既存クラスに resolveInstanceMethod: を実装して、存在しないメソッドが必要になったときに動的追加する方法を紹介しました。

iOS 5で追加されたメソッドをすべて疑似メソッドとしてiOS 4上で実装できるわけはないですし、presentingViewController のように挙動を完璧にトレースできないメソッドもありますが、ファイルの追加・削除でコントロールできるのでちょっとした互換性確保には使える方法だと思います。

dismissViewControllerAnimated:completion: なんかもそれっぽい動きをするものを実装できたので、機会があったら紹介したいと思います。

*1:引数型 "@@:" は http://news.mynavi.jp/column/objc/020/index.html を参考に決定しました