その後のその後

iOSエンジニア 堤 修一のブログ github.com/shu223

Method Swizzling をうまく使っている実用例

Method Swizzlingは、既存のメソッドの実装を、自前の実装に差し替えるための手法です。


・・・ということを知ってはいても、どういうときに使うと便利なのかイマイチわかってなかったので、Method Swizzlingをうまく使った実用例を2つほど探してきました。

実用例その1:既存ソースコードに手を入れずに機能追加

xib ファイルのローカライズを IB 上でできるようにする AutoNibL10n

通常、xibで作成したUIをローカライズする場合、

  • xibファイルを言語ごとに用意する
  • アウトレットを定義してプログラム側からローカライズした文言をセットする

といった面倒な作業が必要でしたが、 AutoNibL10n を使用すると、xibファイルを IB から直接多言語対応できるようになります。


たとえば、RootViewController.xibというファイルがあり、その中のUILabelオブジェクトを多言語化したい場合、直接 IB から UILabel のtextプロパティに "mytext" を指定すれば、Localizable.strings に"mytext"というキーで定義された各言語の文字列が挿入されるようになります。

AutoNibL10n における Method Swizzling

この AutoNibL10n、使用するために新たにコードを書く必要は一切なく、ヘッダをインポートする必要すらありません。


どうやっているかというと、Method Swizzling を利用して、load メソッドがコールされるタイミングで(=メモリにアプリケーションがロードされるタイミングで) NSObject の awakeFromNib メソッドを自前の localizeNibObject メソッドに入れ替えることで実現しています。

+(void)load {
    Method localizeNibObject = class_getInstanceMethod([NSObject class], @selector(localizeNibObject));
    Method awakeFromNib = class_getInstanceMethod([NSObject class], @selector(awakeFromNib));
    method_exchangeImplementations(awakeFromNib, localizeNibObject);
}


awakeFromNib の差し替え後のメソッド localizeNibObject の実装はこんな感じです。

-(void)localizeNibObject {
    LocalizeIfClass(UIBarButtonItem);
    else LocalizeIfClass(UIBarItem);
    else LocalizeIfClass(UIButton);
    else LocalizeIfClass(UILabel);
    else LocalizeIfClass(UINavigationItem);
    else LocalizeIfClass(UISearchBar);
    else LocalizeIfClass(UISegmentedControl);
    else LocalizeIfClass(UITextField);
    else LocalizeIfClass(UITextView);
    else LocalizeIfClass(UIViewController);

    if (self.isAccessibilityElement == YES)
    {
        self.accessibilityLabel = localizedString(self.accessibilityLabel);
        self.accessibilityHint = localizedString(self.accessibilityHint);
    }

    [self localizeNibObject]; // actually calls awakeFromNib as we did some method swizzling
}


この中でコールされている localizedString メソッドで、NSBundle の localizedStringForKey:value:table: メソッドを用いて Localizable.strings に定義されたローカライズ文字列を取得しています。

return [[NSBundle mainBundle] localizedStringForKey:aString value:nil table:nil];

実用例その2:iOSバージョンの違いを吸収する

UIRefreshControl を iOS5 でも使えるようにする ISRefreshControl

ISRefreshControl は、iOS6で導入された UIRefreshControl を iOS5 でも使えるようにするライブラリです。


UIRefreshControl と同じ API なので、導入する側は UIRefreshControl と同じように実装するだけ

self.refreshControl = (id)[[ISRefreshControl alloc] init];
[self.refreshControl addTarget:self
                        action:@selector(refresh)
              forControlEvents:UIControlEventValueChanged];

これだけで、iOS6 で動作させると UIKit の UIRefreshControl が呼ばれ、iOS5 で動作させると ISRefreshControl (UIRefreshControl と同じ見た目、挙動)が呼ばれます

ISRefreshControl における Method Swizzling

UITableViewController+RefreshControl というカテゴリが用意されていて、これの load メソッド内で、次のように Method Swizzling が行われています。

+ (void)load
{
    @autoreleasepool {
        if ([[[UIDevice currentDevice] systemVersion] hasPrefix:@"5"]) {
            Swizzle([self class], @selector(refreshControl), @selector(iOS5_refreshControl));
            Swizzle([self class], @selector(setRefreshControl:), @selector(iOS5_setRefreshControl:));
        }
    }
}

(※Swizzle()は同ファイルに定義されているMethod Swizzlingのラッパー関数)
iOSのバージョンが "5" だった場合に、UITableViewController の refreshControl というプロパティの setter と getter を差し替える実装になっていることがわかります。


iOS5のときに差し替える実装は、

- (ISRefreshControl *)iOS5_refreshControl
{
    return objc_getAssociatedObject(self, @"iOS5RefreshControl");
}

- (void)iOS5_setRefreshControl:(ISRefreshControl *)refreshControl
{
    objc_setAssociatedObject(self, @"iOS5RefreshControl", refreshControl, OBJC_ASSOCIATION_RETAIN);
}

となっていて、Associated Objectの仕組みを利用して iOS5 で UITableViewController の refreshControl というプロパティにアクセスできるようにしています。

で、KVOでrefreshControlプロパティを見張り、

[self addObserver:self
       forKeyPath:@"refreshControl"
          options:options
          context:NULL];

refreshControlプロパティに値(UIViewサブクラスのオブジェクト)がセットされたときに、所定の位置にそのビューを貼付ける、という実装になっています。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == self && [keyPath isEqualToString:@"refreshControl"]) {
        UIView *oldView = [change objectForKey:@"old"];
        UIView *newView = [change objectForKey:@"new"];
    
        if ([oldView isKindOfClass:[UIView class]]) {
            [oldView removeFromSuperview];
        }
        if ([newView isKindOfClass:[UIView class]]) {
            newView.frame = CGRectMake(0, -50, self.view.frame.size.width, 50);
            newView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
            [newView setNeedsLayout];
            [self.view addSubview:newView];
        }
        return;
    }
    // 後略
}

このように、Method Swizzling を用いることで、導入する側は iOS のバージョン判定も、このライブラリ独自のAPIを使う必要もなく、UIRefreshControlと同じように実装するだけ、というカンタン導入を実現しています。

Method Swizzlingの参考書籍

日本語の本だと、『iOS SDK Hacks』に解説と事例が掲載されています。


iOS SDK Hacks ―プロが教えるiPhoneアプリ開発テクニック
吉田 悠一 高山 征大 UICoderz
オライリージャパン
売り上げランキング: 226658

まとめ

Method Swizzlingをうまく使っている実用例を2つ紹介しました。

これ以外にも、単体テストで使える OCMock というライブラリも、Method Swizzling を利用して既存オブジェクトのメソッドを差し替えてテストできるようにしていたりと、Github上にはたくさんの実用例が転がっているようなので、また追記したいと思います。