[iOS]ReactiveCocoaFramework入門 | アドカレ2013 : SP #11
この記事ではXcode5とARCを使用することを前提にしています。
はじめに
iOSやMacOSで提供されるOSのフレームワークでは
- UIイベントをキャッチする
- インスタンスのプロパティ等を監視する
- サーバにリクエストを投げてレスポンスが待つ
- 非同期で何か重たい処理を行う
といった外部で起こる時々刻々の変化を捉えるために様々な方法が提供されています。
- Delegate
- Selector
- Key Value Observing
- Notification Center
- Grand Central Dispatch
アプリに要求されるパフォーマンス等と相談しながらこれらの仕組みを使い分けるのも非常に重要ですが、
これら時々刻々の値の流れを捉え、一括して管理しやすく扱うための包括的なフレームワークがあります。
それがReactiveCocoaFrameworkです。
ReactiveCocoaの導入
通常のインストール法
ひとまずReactiveCocoaをプロジェクトに導入してみましょう。
まずプロジェクトにReactiveCocoaのソースをダウンロードします。ここではターミナルからプロジェクトのディレクトリへ移動し、そこでGithubからソースをリポジトリごとダウンロードします。
$ git submodule add https://github.com/ReactiveCocoa/ReactiveCocoa
ソースがプロジェクトにダウンロードされたら次にダウンロードされたReactiveCocoaのフォルダ配下に移動し、次のようにシェルスクリプトを動作させます。
$ cd ReactiveCocoa $ sh script/bootstrap
ここでxctoolが入っていないと怒られるので注意しておきます。xctoolは
$ brew install xctool
などでインストールできます。
次にReactiveCocoa/ReactiveCocoaFrameworkディレクトリ内にあるReactiveCocoa.xcodeprojをプロジェクト内にドラッグ&ドロップします。
インポートを完了したらTarget->Build Phases->Link Binary With LibrariesからlibReactiveCocoa-iOS.aの実行ファイルをインポートします。
Target->Build Settingsの検索窓からHeader Searchと入力してHeader Search Pathの左から2番目のテクストボックスをダブルクリックし、$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/includeのパスを追加します。
同じくTarget->Build Settingの検索窓からOther Linkerと入力してOther Linker Flagsの左から2番目のテクストボックスに-ObjCのフラグを追加します。
最後にTarget->Build Phases->Target DependenciesにReactiveCocoa-iOSを加えて準備完了です。
CocoaPodsを用いた方法
CocoaPodsを用いれば
pod 'ReactiveCocoa'
という内容のPodfileをプロジェクトファイルの入ったディレクトリに置き、プロジェクトの置かれたディレクトリ上で
$ pod
と叩くと[プロジェクト名].xcworkspaceファイルが作られ、ReactiveCocoaが利用可能になります。
ReactiveCocoaを動かす
実際に概念の説明の前にReactiveCocoaを動かしてみます。
クラスの概念等を把握してからコードを読みたい方は一つ下の「ReactiveCocoaの部品解説」から読むことをおすすめします。
Single View Application プロジェクトにReactiveCocoaをインポートした上で以下のコードをViewController.mに書いてビルトしてみてください。
Example1 ~UITextFieldの値を監視する~
ViewController.m
#import "ViewController.h" #import <ReactiveCocoa/ReactiveCocoa.h> @interface ViewController () <UITextFieldDelegate> @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 100, 120, 40)]; textField.delegate = self; [self.view addSubview:textField]; RACSignal *signal = RACObserve(textField, text); [signal subscribeNext:^(id x) { NSLog(@"value changed to %@", x); }]; textField.text = @"sample1"; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } @end
textFieldのtextの値をKeyboardでreturnを押して変えるたびにコンソールに新しい値が出力されます。RACObserve(textField, text)や[signal subscribeNext]というコードでtextFieldTextDidChangeやKey Value Observingを用いて実装できる機能が追加されていることがわかります。
Example2 ~UIButtonのTouchイベントを取得する~
ViewController.m
#import "ViewController.h" #import <ReactiveCocoa/ReactiveCocoa.h> @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect]; button.frame = CGRectMake(120, 200, 80, 40); [button setTitle:@"Push" forState:UIControlStateNormal]; [self.view addSubview:button]; button.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) { NSLog(@"Tapped"); RACSignal *retSignal = [RACSignal empty]; return retSignal; }]; } @end
Pushボタンを押すたびにTappedという文字列がコンソールに出力されます。16~20行目のコードで[button addTarget: action: forControlEvents:]メソッドを用いて実装できる機能が追加されています。新しいメソッドを書く必要はありません。
Example3 ~コレクションクラスを制御する~
ViewController.m
#import "ViewController.h" #import <ReactiveCocoa/ReactiveCocoa.h> @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSArray *charArray = [@"A B C D E F" componentsSeparatedByString:@" "]; RACSequence *charSequence = charArray.rac_sequence; RACSequence *doubleCharSequence = [charSequence map:^id(NSString *value) { return [value stringByAppendingString:value.lowercaseString]; }]; RACSignal *signalFromDoubleCharSequence = [doubleCharSequence signal]; [signalFromDoubleCharSequence subscribeNext:^(id x) { NSLog(@"%@", x); }]; } @end
コードを動かすとAa Bb Cc Dd Ee Ffと順々に各アルファベットがコンソールに出力されていきます。この時、同じことを通常のコードで行うときに書きそうなfor文は一回も使っていないことに注目してください。
ReactiveCocoaの部品解説
上のサンプルコードで登場したクラスやメソッド等を一つ一つ解説していきます。
RACSignalクラス
時々刻々とした値の流れを表すクラス
将来変更の可能性がある値等にビーコンのように常駐させることができます。
タッチイベントが起こった、サーバからレスポンスがあった、テキストフィールドの内容が変更された。等のイベントはこのクラスにすべて集約され、購読(Subscribe)を行うと購読開始時及び値が変更された時にSignalにくるまれた値を受け取り、その値に応じた副作用を追加できます。
変更後の値にアクセスするためにはユーザはシグナルを購読する必要があります。
上のサンプルコードのRACSignalインスタンスを確認してみましょう。
- Example1.l15のsignalインスタンスはtextFieldのtextプロパティの内容を常に監視しています。値が変更されるごとに購読者に新しい値を通知してくれます。(l.16-18)
- Example2.l19のretSignalインスタンスは何も起こさないSignalです。emptyメソッドについては後述します。
- Example3.l16のsignalFromDoubleCharSequenceインスタンスはRACSequenceのシグナルを常しています。Sequenceが遅延評価(後述)されて初めてSignalにくるまれた内容を購読者に順々に伝えていきます。
[RACSignal]subscribeNext:(void (^)(id x))メソッド
RACSignalクラスのインスタンスを購読して、Signalから通知があった時にSignalにくるまれた値(x)を取得、ブロック内で副作用を含んだ処理を行います。尚、Signalから購読できるイベントには3つの種類があります。
- next イベント・・・Signalから提供される新しい値を取得できるイベント
- error イベント・・・Signalから新しい値を通知する際に起こったエラーを通知するイベント
- completed イベント・・・Signalが正常に終了し、かつこのSignalから新しいnextイベントが提供されないことを示すイベント
各イベントごとにそれぞれsubscribe〜メソッドが備わっており、それぞれにイベントに応じた副作用のブロックを付与できます。
サンプルコードのsubscribeNextメソッドを確認していきます。
- Example1.l16のsubscribeNextメソッドはtextFieldインスタンスのtextプロパティを監視するsignalインスタンスからプロパティが更新された時に通知されるnextイベントを受け取った時の副作用を記述しています。signalから送られるxオブジェクトは監視対象のtextField.textプロパティです。
- Example3.l18のsubscribeNextメソッドではdoubleCharSequenceからのSignalを表しているsignalFromDoubleCharSequenceを購読しています。送られてきた値をコンソールに出力する処理がブロックの中に書かれています。doubleCharSequence.signalからどのタイミングで値が送られるかについては後述します。
RACObserveマクロ
Key Value Observingと同じように各インスタンスに紐ついたプロパティが購読開始、もしくは変更された時に新しくnextイベントを生成するRACSignalインスタンスを生成しています。
- Example1.l15でRACObserve(textField, text);と書いてある部分ではtextFieldインスタンスのtextプロパティの値を監視するRACSignalインスタンスを生成しています。
RACCommandクラス
UIにアクションを起こした時などにRACSignalを生成する命令を表すクラスです。UIにイベントが送られるたびにexecuteメソッドが呼ばれ、初期化時に設定されたブロックを実行するとともに新しいRACSignalを生成するような処理を記述できます。
[UIButton]rac_commandプロパティ
UIButtonがUIControlEventTouchUpInsideの状態になった時のRACCommandをsetできます。setされたRACCommandはその状態になるたびにexecuteされ、初期化ブロックを実行して新しくRACSignalを生成します。
[RACCommand]initWithSignalBlock:(RACSignal * (^)(id input))メソッド
RACCommandを生成予定のRACSignalとともに生成する初期化メソッドです。inputにはRACCommandインスタンスがプロパティとしてsetされるインスタンスに応じた値が入ります。
- Example2.l17の初期化部分でinputにはRACCommandがプロパティにセットされたbuttonインスタンスが出力されます。ボタンを押すたびに新しいRACSignalが生成され、そのたびに初期化時に書かれたブロックが実行されます。
[RACSignal]emptyメソッド
即時completeイベントを通知するSignalを提供するメソッドです。
- Example2.l18のretSignalがcompleteイベントを通知しているかどうかはl19の直後に
[retSignal subscribeCompleted:^{ NSLog(@"complete") }];
と追加することで確認できます。
RACSequenceクラス
普段使っているNSArrayのような配列にmap,filter,fold,reduceなどの関数型言語に見られるような高階関数を加え、遅延評価(式を宣言した時にではなく、値が必要になった時に初めて計算を行う)をデフォルトの式評価法としたコレクションクラスです。
Objective-CのコレクションクラスであるNSArrayやNSDictionary,NSEnumaratorクラスからはrac_sequenceプロパティから簡単にRACSequenceインスタンスを取得できます。
signalメソッドからRACSignalを取得した段階で配列全体からQueueがDISPATCH_QUEUE_PRIORITY_DEFAULTに従って作成され、Queueの優先順序に従い評価が実行されます。評価が実行されるたびに配列の要素がシグナルから順次通知されていきます。
- Example3.l11ではcharArrayから生成されたRACSequenceインスタンスをcharSequenceに渡しています。
- Example3.l12-15ではcharSequenceの各要素からmapされた要素で作られた新しいRACSequenceインスタンスをdoubleCharSequenceに渡しています。
[RACSequence]map:(id (^)(id value))メソッド
RACSequenceの各要素をマッピングした要素からなる新たなRACSequenceを生成します。
- Example3.l13-15ではcharSequenceの配列@[@"A",@"B",@"C",@"D",@"E",@"F"]の各要素を受け取り末尾に元の要素を小文字にした要素を追加するBlock
^id(NSString *value) { return [value stringByAppendingString:value.lowercaseString]; }
によって新たなRACSequenceインスタンスを生成しています。
まとめ
まとめと言ってはなんですが、今まで紹介してきたExampleを組み合わせてUITextFieldにあるテキストをスペース文字で区切って、Pushボタンを押すたびにコンソールにそれぞれ二階づつ出力するサンプルを示します。いままでのコードの組み合わせですので詳細は割愛します。
ViewController.m
#import "ViewController.h" #import <ReactiveCocoa/ReactiveCocoa.h> @interface ViewController () <UITextFieldDelegate> @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 100, 120, 40)]; textField.borderStyle = UITextBorderStyleLine; textField.delegate = self; textField.backgroundColor = [UIColor whiteColor]; [self.view addSubview:textField]; UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect]; button.frame = CGRectMake(120, 200, 80, 40); [button setTitle:@"Push" forState:UIControlStateNormal]; button.alpha = 0.4f; button.userInteractionEnabled = NO; [self.view addSubview:button]; RACSignal *signal = RACObserve(textField, text); [signal subscribeNext:^(NSString *textFieldText) { BOOL enableButton = [textFieldText componentsSeparatedByString:@" "].count >= 2; button.alpha = enableButton ? 1.0f : 0.4f; button.userInteractionEnabled = enableButton; }]; button.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) { NSArray *stringArray = [textField.text componentsSeparatedByString:@" "]; RACSequence *stringSequence = stringArray.rac_sequence; RACSequence *resultSequence = [stringSequence map:^id(NSString *value) { return [value stringByAppendingString:value]; }]; [resultSequence.signal subscribeNext:^(id x) { NSLog(@"%@",x); }]; return [RACSignal empty]; }]; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } @end
ここで同じ機能を持ったコードを従来の方法で実装する方法を考えてみてください。 ViewControllerクラスにフィールドをたくさん持たせる必要が出てきて、管理に多少手間のかかるコードになるはずです。
最後に
ReactiveCocoaフレームワークを用いるとプログラマがクラスに付随する状態を管理する必要が以前よりも少なくなり、従来の方法よりも簡潔でバグの少ないコードが書ける可能性があります。
このような時々刻々の変化を捉えて、インプットからアウトプットへの処理の流れを予め定義しておく流儀のプログラミング手法はFRP(Functional Reactive Programming)とも深い関連があるのですが、この記事では参考サイトの紹介にとどめておきます。