こんにちは、iOSプログラマーの_ishkawaです。
このエントリは tech.kayac.com Advent Calendar 2012 8日目の記事です。
テーマは「私の中のマイイノベーション 2012」です。
12月。恋人たちが心の溝を埋めていく中、僕はiOSバージョンの溝を埋めております。
今日はそのテクニックを紹介したいと思います。
それと、紹介するテクニックを使ったマイイノベーションも紹介します。
基本中の基本
iOSでは、バージョンによってクラス/メソッドの有無やプロトコルへの適合状況が異なります。
これらの状況の違いは以下の方法で判別することができます。
- メソッドが存在するかどうか:
respondsToSelector:
- クラスが存在するかどうか:
[Class class]
- プロトコルに適合しているか:
conformsToProtocol:
これらの条件を以下のように利用することで、バージョンに応じた処理を行うことができます。
if ([hoge respondsToSelector:@selector(fuga)]) {
// fugaメソッドがある場合の処理
} else {
// ない場合の処理
}
Method Swizzling
Method Swizzlingは既存のメソッドを置き換えるテクニックで、Objective-Cハッカーにはお馴染みのものです。
このテクニックの面白いところは"動的"であるという点です。
iOS5未満ならメソッドを置き換えるが、iOS6以上なら置き換えないといったことが可能なのです。
利用手順
Method SwizzlingではObjective-C runtime APIを利用するので<objc/runtime.h>
をインポートします。
#import <objc/runtime.h>
既存のセレクタ(original
)と新しいセレクタ(alternative
)を用意し、
実装状況に応じてclass_replaceMethod
またはmethod_exchangeImplementations
を呼ぶ関数を用意します。
static void MethodSwizzle(Class c, SEL original, SEL alternative)
{
Method orgMethod = class_getInstanceMethod(c, original);
Method altMethod = class_getInstanceMethod(c, alternative);
if(class_addMethod(c, original, method_getImplementation(altMethod), method_getTypeEncoding(altMethod))) {
class_replaceMethod(c, alternative, method_getImplementation(orgMethod), method_getTypeEncoding(orgMethod));
} else {
method_exchangeImplementations(orgMethod, altMethod);
}
}
+ (void)load
などのメソッドをオーバーライドし、メソッドの実装を差し替えます。
+ (void)load
{
if ([[[UIDevice currentDevice] systemVersion] hasPrefix:@"4"]) {
MethodSwizzle([NSObject class], @selector(hoge), @selector(fuga));
}
}
このように実装すると、[object hoge]
を呼び出したときに[object fuga]
が実行されます。
実用例: UIWebView
のscrollView
プロパティの差を埋める
iOS5以降ではUIWebView
にscrollView
プロパティが用意されていますが、それ以前のバージョンでは用意されていません。
decelerationRate
やscrollsToTop
を変更する機会は少なくないので、同じインターフェースで呼べるようにしたいものです。
これを実現するには以下のようなカテゴリを実装します。
@implementation UIWebView (iOS4ScrollView)
+ (void)load
{
if ([[[UIDevice currentDevice] systemVersion] hasPrefix:@"4"]) {
MethodSwizzle([UIWebView class], @selector(scrollView), @selector(iOS4_scrollView));
}
}
- (UIScrollView *)iOS4_scrollView
{
UIScrollView *scrollView = nil;
for (UIView *subview in [self subviews]) {
if ([subview isKindOfClass:[UIScrollView class]]) {
scrollView = (UIScrollView *)subview;
break;
}
}
return scrollView;
}
@end
このカテゴリがロードされると、バージョンによらず以下の方法でscrollView
にアクセスできるようになります。
UIWebView *webView;
UIScrollView *scrollView = webView.scrollView;
Method Swizzlingを利用すると、この例の他にも以下のようなものも実現できます!
UIImage
のNSCoding
プロトコルへの適合状況の差を埋めるUIViewController
のparentViewController
の差を埋める
メリットと注意点
iOSバージョンの溝を埋めることには呼び出し側がバージョンの差を意識する必要がなくなるというメリットがあります。
一方で、複数人で作業する場合に思わぬ誤解を与える原因になるというデメリットもあります。
なので、このテクニックを利用する場合にはドキュメントやコメントに明記することを強く勧めます。
マイイノベーション: ISRefreshControl
iOS6からUIRefreshControl
という非常に良い感じのUIコンポーネントが提供されました。
しかし、iOS5ユーザーもまだまだ多い現状では、これだけを理由にDeployment Targetを6.0にするわけにもいきません。
そこで、iOS5でもおおよそ同等の動きをするISRefreshControl
というライブラリをつくりました。
先ほど説明したMethod Swizzlingを利用してiOS5, iOS6の溝を埋めています。
利用手順
UITableViewController
で以下のように設定を行います。
UIRefreshControl
の使い方と2文字しか変わりません!(id型キャストを入れると6文字です。)
self.refreshControl = (id)[[ISRefreshControl alloc] init];
[self.refreshControl addTarget:self
action:@selector(refresh)
forControlEvents:UIControlEventValueChanged];
どう動くか
- iOS6: 本物の
UIRefreshControl
として動作します。(コンストラクタがUIRefreshControl
のインスタンスを返します。) - iOS5:
ISRefreshControl
として、UIRefreshControl
の真似をします。
どう実装したのか
- iOS6:
+ (id)alloc
でUIRefreshControl
を返す。 - iOS5:
UITableViewController
を拡張してrefreshControl
プロパティを用意。 - iOS5:
tableView
のcontentOffset
をキー値監視してISRefreshControl
にスクロール量を通知。 - iOS5:
CGPath
でスクロール量に応じた"びよーん"を描画。
コードの解説は長くなりそうなので控えますが、こんな感じでUIRefreshControl
の溝を埋められるのです!
まとめ
- iOSのバージョンの溝は頑張れば埋められることもある。
- 頑張って溝を埋めたらドキュメントにちゃんと書く。
さて、明日はマッドプログラマーの@9reさんのお話です。
今日はなんだか地味なエントリーだな〜って思った方も明日は楽しめると思います!