2015年を振り返ってみる

2015年も残すところあと数時間。aikoの年越しライブ Love Like Pop 18に向かっている途中/開演待ちに書いています。

色んなところで振り返りエントリーが流れているので、自分も振り返ってみようかと書いてみます。

フリュー

昨年10月から業務委託としてフリュー株式会社のとある部署・チームに参画し、Swift/ReactiveCocoaをベースにiOSアプリの開発をしていました。Swiftのバージョンアップへの追従、非同期処理周りをBoltsからReactiveCocoaに置き換えるなどはかなり気合いで進めました。後述しますが、自作のHimotokiというライブラリーもこのアプリで本番投入しています。また環境面ではGitHubでPRベースでのレビュー体制やSlackをそれなりに働きかけて導入してもらったり、4KディスプレイやMBPのスタンドを用意してもらったり、チームメンバーも含めて非常にいい環境で仕事をさせてもらうことができました。

こちらについては去る12月25日が最終出社日でした。1月からの活動には乞うご期待。

関西モバイルアプリ研究会

今年はここへの参加がかなり大きなトピックでした。記念すべき第1回に id:yashigani_w さんから声をかけてもらってLT参加し、以来12月の第9回まで皆勤で参加させてもらっています。これまでTwitterでは知っていたiOS開発者の方々、交流のなかったAndroid界隈の方々と絡む機会ができたのもこの会のお陰です。来年の関モバにも期待していきましょう!

Himotoki

SwiftJSONデコード用のHimotokiというライブラリーを開発、5月に公開しました。それなりの規模のライブラリーを公開したのはこれが初めてで、現時点で300を超えるスターが付いているのはなかなか頑張れたかな、と思っています。関モバでも布教LTをしたり、国内で色んな所で使ってもらえているような話を聞くので、今後もちゃんとメンテナンス、Swiftのバージョンアップ追従を頑張っていく所存です。

ちなみにこれを作るまではObjectMapperというのを使っていたんですが、以下のやり取りで自分で作ってみようと思い立つことができました。@_ishkawa さんに感謝🙏

Carthage/ReactiveCocoa

昨年後半に登場し、今年1年でかなり普及した感のあるCarthageですが、PRを幾つか重ねている内にコミッターになりました。今年6月のことです。

最近のリリースでは結構多くの貢献を自分がしている状態だったりします。今後も開発者のための効率向上を目指してコミッター業をしっかりやっていくつもりなので、バグレポートや分からないところがあればお問い合わせいただければと。

またCarthageにも使われているReactiveCocoaのコミッター業も細々と続けています。元々CarthageはSwift版のRAC 3.0のショーケース/実践投入によるAPI洗練のためでもあったので、RAC側のバグ修正をすることもしばしばありました。RxSwiftだけでなく、RACもぜひぜひ試してほしいですね。

来年

先かと思っていたらあと2ヶ月程度になってしまいました。来る3月2日(水)〜4日(金)に東京・渋谷で行われるtry! Swiftカンファレンスに登壇します!!それも初日のトップバッターというとても緊張する発表順になりました。

国内では初めての大規模なSwiftに関するカンファレンス、それも海外からの多数の登壇者やオープンソース化後初めてのカンファレンスということもあり、非常にエキサイティングな3日間が過ごせるのではととてもワクワクしています。😄

スーパーSwift早割は終わってしまっていますが、まだSwift早割のチケット、また1日チケットも用意されているので、沢山の参加者にお会いできることを楽しみにしています。🙇🏻

まとめ

年越しライブ楽しんでくるぞ!!

Carthageのコミッター(Collaborator)になりました

先月の2015年6月に、CarthageというCocoaプラットフォーム(iOS/OS X/watchOS)用の依存性管理ツールコミッター(Collaborator)になりました

このプロジェクトは元々ReactiveCocoa 3.0APIを洗練させるためのショーケースとして始まったという側面もあり、ReactiveCocoaのCollaboratorとしては当然注目していました。Carthage v0.6.5からv0.7へのバージョンアップ時に、Swift 1.1から1.2への移行、それに伴うReactiveCocoaのアップデートがあったんですが、そこでRACAPI/設計が大きく変わったことで結構なバグが発生してしまっていました。それらのバグなどを調査・修正して色々とcontributeしている内にinviteしてもらった、という次第です。

RAC 3に慣れていないとなかなか修正し辛いコードベースなのは確かだと思うので、RACのCollaboratorでもあることを活かして貢献していくつもりです。Carthageユーザーで何かお困りの方はこのブログや@ikesyo経由で是非フィードバックをいただければと思っています。

逆引き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ユーザーは利用してみてください(どれだけいるのか)。

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

逆引きReactiveCocoa: シグナルにイベント発生時に対する処理を挟み込む

ReactiveCocoaのドキュメントにもあるように、-do...系のメソッドを使用すると、シグナルを購読せずに副作用を挟み込むことができます。

RACSignal *newSignal = [[[signal 
    doNext:^(id x) {
        NSLog(@"next");
    }] 
    doError:^(NSError *error) {
        NSLog(@"error");
    }] 
    doCompleted:^{
        NSLog(@"completed");
    }];

シグナルを購読する終端であれば、各種処理を-subscribe...:系のメソッドのブロック内で行うこともできますが、シグナルに手を加えてシグナルを返すようなメソッドでは便利に使用することができます。

また、例えば-doNext:を重ねて記述した場合、記述順(lexical order)に処理が実行されます。

[[[signal
    doNext:^(id x) {
        NSLog(@"1st");
    }]
    doNext:^(id x) {
        NSLog(@"2nd");
    }]
    subscribeNext:^(id x) {
        NSLog(@"-subscribeNext:");
    }];
// 1st
// 2nd
// -subscribeNext:

シグナルの終了時の処理として正常系/異常系のどちらでも同じ処理を行いたい場合、-finally:が使用できます。

// 以下の2つは同義。

[signal finally:^{
    [self doSomeStuff];
}];

[[signal
    doError:^(NSError *error) {
        [self doSomeStuff];
    }]
    doCompleted:^{
        [self doSomeStuff];
    }];

補足

ReactiveCocoa 3.0では-finally:は非推奨となり、-doFinished:が追加(としてリネーム)されています。