読者です 読者をやめる 読者になる 読者になる

Swiftでジェネリックなメソッドの特殊化を行う方法

今日色々とやり取りをしながら、Swiftでのジェネリックメソッドについてある知見を得たのでまとめておきます。

以下のようなSwiftの型・ジェネリックメソッドがある時、このままでは型指定(特殊化)が面倒になってしまいます。

class Hoge {
    class func genericMethod<T: Request>(callback: T -> Bool) {
        ...
    }
}

// 呼ぶ時
Hoge.genericMethod { (x: SomeRequest) in true }

上記のような場合、型情報のヒントが与えられないのでクロージャでパラメータの型を明示しないといけません。引数にTインスタンス・値を渡す場合は型推論が効くし、以下のように型自体がジェネリックな場合は初期化時に明示的に特殊化が出来るのですが。。

class Generic<T> {}

let generic = Generic<String>()

このようなケースでは、メソッドのパラメータとして型自体を渡させるようにするとそれが型のヒントとなりクロージャで型指定を行う必要がなくなります。

class Hoge {
    class func genericMethod<T: Request>(type: T.Type, callback: T -> Bool) {
        ...
    }
}

// 呼ぶ時
Hoge.genericMethod(SomeRequest.self) { x in true }

振り返ってみればObjective-CではこのようにClassをパラメータで渡すことも多かった気がしますね。

このパターンを使うと、例えばUITableViewCellのdequeueが以下のように出来、キャストが不要になります。

extension UITableView {
    func registerNibForClass<T: UITableViewCell>(type: T.Type) -> UINib {
        let nib = UINib(nibName: type.nibName(), bundle: nil)
        registerNib(nib, forCellReuseIdentifier: type.reuseIdentifier())
        return nib
    }

    func dequeueCell<T: UITableViewCell>(type: T.Type, forIndexPath indexPath: NSIndexPath) -> T {
        return dequeueReusableCellWithIdentifier(type.reuseIdentifier(), forIndexPath: indexPath) as T
    }
}

extension UITableViewCell {
    class func simpleClassName() -> String {
        return NSStringFromClass(self).componentsSeparatedByString(".").last!
    }

    class func nibName() -> String { return simpleClassName() }

    class func reuseIdentifier() -> String { return simpleClassName() }
}

let cell = tableView.dequeueCell(HogeCell.self, forIndexPath: indexPath)

Javaとかでは出来たような気がしますが Hoge.genericMethod<SomeRequest> { x in true } みたいにメソッドコール時に明示的に特殊化が出来ればいいんですけどね。

逆引きReactiveCocoa: 固定値をRACSignalでラップする

非常に基本的なことですが、あまりはっきりと触れられていない(触れる必要がない?)のが、単純に固定値をラップしたシグナルを生成するケースです。方法自体は単純で、以下のようなメソッドが用意されています。

// 値の場合
RACSignal *singleValueSignal = [RACSignal return:@"foobar"];

// 下記と同義
[RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"foobar"];
    [subscriber sendCompleted];
    return nil;
}];

// `completed`だけの場合
RACSignal *emptySignal = [RACSignal empty];

// 下記と同義
[RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    [subscriber sendCompleted];
    return nil;
}];

// `error`だけの場合
NSError *someError = ...;
RACSignal *errorSignal = [RACSignal error:someError];

// 下記と同義
[RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    [subscriber sendError:someError];
    return nil;
}];

利用ケースについて、例えば引数のシグナルの変化に合わせて指定したセレクターを実行する -[NSObject rac_liftSelector:withSignals:] では、引数は全てシグナルでなければならないのですが、

[self.mapView rac_liftSelector:@selector(setCamera:animated:) withSignals:cameraSignal, [RACSignal return:@YES], nil];

というように、固定値を使いたい場合などにはよく使う必要が出てくるのではないでしょうか。

ReactiveCocoaのコミッターになりました(なってました)

もう結構前で昨年末か年始からなのですが、自分でもいくつか解説を書いている ReactiveCocoa というObjective-C用のFunctional Reactive Programmingライブラリのコミッターになりました!

ReactiveCocoa

ちなみに現時点までで19個のプルリクをしていて、マージされたのが15個、クローズされたのが3個、未マージなのが1個という感じです。

以下一覧

結構細かい改善・リファクタリングや機能追加が多いですが、こんな感じの貢献でもスター4,300超えのプロジェクトのコミッターになれるという一例になればと思います。

これからも逆引きReactiveCocoaの記事を書いたり、日本語での情報発信も増やしていければと考えています。日本でRACをお使いの方はどしどしご質問ください!

それでは Let's use ReactiveCocoa!

P.S. 絶賛お仕事も募集中です! (特にReactiveCocoaを使えるiOSなお仕事)

ACAccountStoreのReactiveCocoaラッパーを作った

タイトルのままですが ikesyo/ReactiveAccountStore というAccounts.framework (ACAccountStore) のReactiveCocoaラッパーを作ってみました。

現時点では、以下の4つの非同期メソッドのRACSignalラップ版をサポートしています。

  • -requestAccessToAccountsWithType:options:completion:
  • -saveAccount:withCompletionHandler:
  • -renewCredentialsForAccount:completion:
  • -removeAccount:withCompletionHandler:

より詳しくはヘッダードキュメントを参照してください。

すでにCocoaPodsに登録済みなので、以下で利用できるようになります。

pod 'ReactiveAccountStore'

対応プラットフォームは、OS Xのサポートとdeprecatedのメソッドの関係上、

となっています。

Accounts.frameworkを使用することがあるReactiveCocoaユーザーは利用してみてください(どれだけいるのか)。

VTAcknowledgementsViewControllerのStoryboardサポートを改善した

Pull requestしたらローカライズ依頼された - [yashigani days] などで知ったVTAcknowledgementsViewControllerというライブラリがあります。CocoaPodsが自動生成してくれる各ライブラリのライセンス表記をまとめたファイルをうまいことアプリ内で画面表示してくれる便利ライブラリです。

CocoaPodsが生成するファイル名は通常、Pods-acknowledgements.plistですが、Podfileで下記のようにターゲットを明示的に指定している場合、

# Podfile
target "TargetName" do
  pod 'VTAcknowledgementsViewController'
end

生成されるファイル名がPods-TargetName-acknowledgements.plistというように変わります。

ライブラリの機能としてコードベースでの表示では、以下のようにしてターゲット入りのカスタムPlistファイル名に対応していたのですが、

NSString *path = [[NSBundle mainBundle] pathForResource:@"Pods-TargetName-acknowledgements" ofType:@"plist"];
VTAcknowledgementsViewController *viewController = [[VTAcknowledgementsViewController alloc] initWithAcknowledgementsPlistPath:path];

元のエントリーで追加されていたStoryboardサポートではカスタムPlistファイル名は未対応でした。ということで、Storyboardでの配置でもカスタムPlistファイル名に対応するPull requestをしたらマージされましたよ、というお話です。

Support custom plist name for Storyboard by ikesyo · Pull Request #21 · vtourraine/VTAcknowledgementsViewController · GitHub

設定方法

  1. 元エントリーの画像のように、Storyboard上にUITableViewControllerを配置し、Custom ClassにVTAcknowledgementsViewControllerを指定する。
  2. 配置したTableViewControllerのUser Defined Runtime Attributes
    • Key Path: acknowledgementsPlistName
    • Type: String
    • Value: ファイル名(上記例では "Pods-TargetName-acknowledgements")

    としたものを追加する。

実装内容

設定方法の通り、対応にはUser Defined Runtime Attributesを使用しています。User Defined Runtime Attributesでセットされた値が実際にプロパティにセットされるのは、initWithCoder:awakeFromNibの間のため、Storyboardベースでの初期化のエントリーポイントをinitWithCoder:からawakeFromNibに変更した上で、プロパティに値がセットされているかどうかでデフォルトのファイル名を使用するか、カスタムファイル名を使用するかどうかを切り分けるようにしています。

注意

2014/04/15現在、CocoaPodsでのリリースには上記対応はまだ含まれていません。CHANGELOGを見る限り、次のリリースとなる ver 0.11 で入るのでリリースを待つか、

# Podfile
pod 'VTAcknowledgementsViewController', :head

Podfileの:headオプションでリポジトリからmasterブランチを直接取得してくれば使えるようになります。

それでは便利なCocoaPodsライフを!

2014/05/05 追記

ver 0.11 がリリースされました!ので、以下だけで本対応が含まれたバージョンが使用できます。

# Podfile
pod 'VTAcknowledgementsViewController'

UAObfuscatedString / バイナリ上での文字列難読化ライブラリー

UrbanApps/UAObfuscatedString

NSLog(@"%@", @"".T.h.i.s._.i.s._.a._.t.e.s.t.dot); 
> This is a test.

NSString *identifier = @"".c.o.m.dot.u.r.b.a.n.a.p.p.s.dot.e.x.a.m.p.l.e;

サンプルを見れば分かるように、NSStringのカテゴリーとして英数字や記号のメソッドが追加されており、メソッド呼び出しによって文字連結で文字列を生成していくというライブラリー。

アプリのバックエンドAPIのURLやAPIキーといった知られたくない情報をそのまま文字列定数としてソースコードに記載してしまうと、アプリのバイナリ解析で見つけられたり書き換えができてしまうので、バイナリ上には単一文字としてしか含まれないようにするというアイデア。

Objective-CのランタイムAPIをReactiveCocoaで解説する

iOS Advent Calendar 2013 14日目担当の @ikesyo です。

といった感じで今年1年で大分有名になってきた感がある ReactiveCocoa というリアクティブプログラミングライブラリーがあります。

このエントリーではその中でもObjective-CのランタイムAPIの利用箇所について解説してみたいと思います。


ランタイムAPI

まずObjective-CのランタイムAPIですが、大変有名なダイナミックObjective-Cでも紹介されているように、<objc/runtime.h>に定義されている、Objective-Cの言語機能を実現しているCの関数郡となります。

代表的には動的なメソッド定義や実装の追加、既存メソッドの実装の差し替え、動的なクラス定義などができ、比較的よく使用されているのがobjc_setAssociatedObject() / objc_getAssociatedObject() によるカテゴリーでのプロパティ実装だと思います。これはReactiveCocoaでも既存クラスの拡張で広く使用されています。

ReactiveCocoaの場合

ReactiveCocoaでのランタイムAPIの使用例として-[NSObject rac_signalForSelector:] および -[NSObject rac_signalForSelector:fromProtocol:]を取り上げます。これらのメソッドは、引数のセレクターのメソッドが実行された際に、メソッドの引数を要素に持つRACTupleを値としてnextを送信するシグナルを返します。

  • 既存メソッドのフック
  • @optionalのデリゲートメソッドに対して使用することでデリゲートパターンを容易にシグナルの世界に持ち込める

といった用途に使用することができる、非常に強力なメソッドです。

このメソッドの実装には以下のように多数のランタイムAPIの機能が使われています。

以下のようなコードの時、

NSObject *object = [[NSObject alloc] init];

RACSignal *signal = [object rac_signalForSelector:@selector(description)];
[signal subscribeNext:^(RACTuple *args) {
    // -description は引数がないため、`args`は空のタプル。
    NSLog(@"-description invoked.");
}];

[object description];

概要を説明すると内部では以下のような処理が行われています。

  1. 対象インスタンス(メッセージのレシーバー)のクラスのサブクラスを動的に生成し、-forwardInvocation:, -respondsToSelector:の実装を差し替える。
    • NSObject_RACSelectorSignalクラスが生成される。
  2. 対象インスタンスのクラスを 1. で動的生成したサブクラスに置き換える。
    • [object class]NSObjectではなく、NSObject_RACSelectorSignalを返すようになる。
  3. RACSubject(値を自由に送れるRACSignalのサブクラス)をAssociated Objectとして保持する。
  4. 以下の2パターン。
    • 対象のメソッド(セレクター)がオリジナルのクラスに実装されていない場合、_objc_msgForward() を実装とするメソッドを追加する。
    • 対象のメソッド(セレクター)がオリジナルのクラスに実装されている場合、既存メソッドの実装をプレフィックス付きの別名セレクターに退避した上で、対象セレクターに紐づく実装を_objc_msgForward()に差し替える。
      • @selector(description) => _objc_msgForward()
      • @selector(rac_alias_description) => オリジナルの実装
  5. 対象メソッドを実行する。
    • [object description]
  6. (-respondsToSelector:の置き換えは説明省略。)
  7. [object description]のメッセージ送信は実態として_objc_msgForward()が実行され、その結果として-forwardInvocation:が実行される。
  8. -forwardInvocation:の置き換えられた実装の中で、オリジナルの実装を実行した後、保持しているRACSubjectに対して-sendNext:を行う。

では該当のソースファイルとなるNSObject+RACSelectorSignal.m(v2.1.8時点)を見てみましょう。

クラス生成

L247-L257では

  • objc_allocateClassPair()
  • objc_registerClassPair()

を使用し、対象インスタンスのクラスをスーパークラスとするサブクラスを動的に生成し、ランタイム上に登録しています。クラス名として、オリジナルのクラス名に _RACSelectorSignal というサフィックスを付加した名前にし、衝突を回避するようにしています。

インスタンスのクラス変更

L259では

  • object_setClass(self, subclass)

を使用し、対象インスタンスのクラスを動的生成したサブクラスに置き換えています。こんなことまで出来て気持ち悪いですね!!

メソッド生成、インスタンスメソッドの差し替え

1.の-forwardInvocation:, -respondsToSelector:の実装差し替えのために、

L58-L117では

  • imp_implementationWithBlock()によるメソッドの実装生成
  • class_replaceMethod()によるメソッドの実装の差し替え

を行っています。

imp_implementationWithBlock()の引数のブロックは、method_return_type ^(id self, self, method_args …)というシグネチャが要求されるものになっています。-respondsToSelector:の場合は以下のようになりますね。

BOOL (^newRespondsToSelector)(id, SEL) = ^ BOOL (id self, SEL selector) {
    return // some bool value;
};

インスタンスメソッドの追加・差し替え

4.の処理としてL154-L195では

  • 既存メソッドがない場合: class_addMethod()によるインスタンスメソッドの追加
  • 既存メソッドがある場合:
    • method_getImplementation()による既存メソッドの実装の取得
    • class_addMethod()による既存メソッドの実装の別セレクターへの退避
    • class_replaceMethod()によるメソッドの実装の差し替え

を行っています。

既存メソッドに対しては、@selector(rac_alias_someSelector:)のように rac_alias_ をプレフィックスとしたセレクターを生成し、それとmethod_getImplementation()で取得したオリジナルの実装を紐付けています。

ここでメソッドの差し替え後の実装としている関数の_objc_msgForward()が全体のキモとなっている部分で、メソッドの実装がこの関数となっていると、メソッド実行に関する情報(メッセージのレシーバー、セレクター、引数、実行後の戻り値など)がNSInvocationインスタンスとして生成され、それを引数として-forwardInvocation:が実行されます。

メッセージフォワーディング

-forwardInvocation:置き換え後の実装の中身の一部では、シグナルの値送信の前のオリジナルの実装の実行のためにNSInvocationを使用しています。

-rac_signalForSelector:_objc_msgForward() => -forwardInvocation: というメッセージフォワーディングの仕組みが利用されているのは、このオリジナルの実装を実行するためです。

引数として渡されたinvocationオブジェクトの.selectorはオリジナルのセレクターを示していますが、このままでは実装が_objc_msgForward()となってしまうので、4.でオリジナルの実装を退避していた別名セレクター(@selector(rac_alias_hogehoge))を.selectorにセットして[invocation invoke];とすることで、メソッドの戻り値型や引数の型・数を意識することなくオリジナルの実装の実行が完了します。

ただし、メソッドの戻り値が構造体の場合、構造体のサイズによって_objc_msgForward()_objc_msgForward_stret()が本来使い分けられないといけないのですが、それを判断するのは難しいことから-rac_signalForSelector:では構造体を戻り値とするメソッドはサポートされていません

KVOという例外

上記が基本的なロジックですが、例外パターンが存在します。それは-addObserver:forKeyPath:options:context:によってKVO(Key-value observing: キー値監視)の対象となったインスタンスです。ReactiveCocoa的には、RACObserve()マクロを使用したインスタンスも内部ではKVOを使用しているため同様となります。

実はKVOも上記のような動的サブクラス生成・インスタンスのクラス変更を行っており、KVOの対象となったインスタンスはランタイム上で NSKVONotifying_ というプレフィックスが付いたクラスになっています。また-classメソッドがオリジナルのクラスを返すようにオーバーライドされています。

ReactiveCocoaでは動的サブクラスの更なるサブクラス化は避けるために、1.の処理の際にL219-L245

  • -classによるクラス取得
  • object_getClass()によるクラス取得

の結果を比較し、異なるクラスであればKVOなどによる動的サブクラス化されているインスタンスだと判断をして-forwardInvocation:, -respondsToSelector:の実装差し替えだけを行うようにしています。

また、KVOサブクラスによる-respondsToSelector:-classで返されるオリジナルのクラスに対する+instancesRespondToSelector:に基づいているようで、そのままでは-rac_signalForSelector:によってKVOサブクラス側に実装されたセレクターには反応してくれないため、それを回避するように実装を差し替えています。

まとめ

カテゴリーや、上記のようなランタイムAPIを使用したObjective-Cの動的特性は非常に強力ですが、こういった処理を行うライブラリーを複数組み合わせてしまうとデバッグが難しくなったり実装の衝突で大変なことになってしまうことも否定できません。

黒魔術は用法・容量を守って正しくお使いください!!


参考リンク

メッセージフォワーディング
KVO
関連するIssues / Pull Requests