画面の表示にmap()
やfilter()
を活用したり、検索バーに入力したテキストからWeb APIを検索して結果をTableViewに表示するようなUIを作るのは難しいです。正直に申し上げると、FRP (Functional Reactive Programing)はUIKit
やNSURLSession
とは何の関係もありません。
このtry! Swift NYCの講演で、iOSデベロッパーの日々の開発にRxSwift(非同期、イベントベースのフレームワーク)がどのように活用できるのかをお話します。単一依存になってしまう代わりに、大きな問題をクリアに解決したいなら、この講演はきっと役に立ちます。
イントロダクション (00:00)
Reactive ExtensionであるRxはObservable SequenceやLINQ形式のクエリオペレータを使った非同期処理やイベントベースのプログラムのためのライブラリです。Rxを使うことで、デベロッパーは非同期のデータをストリームとして扱うことができます。
(ここでメモを破り捨てる演出)私はこの理論を何度も読みました。ですが、それが日々のiOSアプリの開発に役立つのかわかりません。理解が完全に間違っているリスクがあるので、RxがiOSアプリでどのように動作しているかをお話します。
Rxのリアクティブな側面 (01:08)
まず、Rxのリアクティブな側面についてお話しします。(変更をプルするのではなく)データをプッシュする機能についてです。
Array < String > (01:22)
StringのCollectionであるArrayは(もちろんこれはStringの配列ですが)扱いやすいです。なぜなら、ループに使えるからです(例えば、要素それぞれに対して何かしたり、forEach
を使うことで、よりSwiftらしく使うことができます)。これは全部いっしょで、作業ブロックであるクロージャを用いることで、個々の要素に対して次々に処理します。
配列の問題は、これが一瞬で行われることです。例えば、配列に後で追加される要素のことは考慮できません。なぜまだ加わっていないものを処理したいのでしょう?
Table View Controllerを見てみましょう。配列があり、画面のTable Viewに表示されています。よさそうです。しかし、ユーザーがプラスボタンをタップして、新しいアイテムを追加(して、配列の要素が4つになったと)すると、問題が発生します。データモデルの配列の要素は4つなのに、UI上では3つしか表示されていません。
ビジネスロジックやデータハンドリングに集中せず、通知や、モデルとUIの同期などのことを考えなくてはなりません。データの古いバージョンで非同期に動作する方法があれば便利です。この瞬間の1回だけではなく。
Observable < Array < String > > (03:22)
Rxはデータ(この場合は配列)をObservableクラスにラップします。このObservableクラスは、簡単に言えば、データを時間軸に展開します。 データに”時間という次元”が加わるのです。
配列の例に戻ってみると、Observableは要素それぞれに対して処理したいデータのブロックを定義します(例えば要素をprintするなど)。これが初期データ(配列の3つのアイテム)に適応されます。そこで、4つ目のアイテムが追加されると、再び、ブロックが実行されます。5個目の要素を追加すると、また実行されます。いつ、どうやってそれが起こるのかを決めることから、振る舞いを決めることに移ります。いつもいくつかのデータがあって、変更やイベントが発生したときに何かしたいと思うでしょう。
かなり線形的になります。これが(私が思う)Rxのすごいところのひとつです。とてもシンプルになります。今何を持っているか、将来どうなるか、昔どうだったかを考える代わりに、シンプルな振る舞いを定義するだけでよいのです。配列の例だと、アイテムのリストがあって、それを画面上のTable Viewに表示したいです。これです。RxSwiftは配列に要素が追加されるたびに動作します。この場合では、データからUIまでとても直線的です。
Observableはすごいことができます。例えば、Text Fieldだったら、(まったく違うものの話をしているのに)かなり近い状況になります。この場合、イベントはユーザーが入力した文字や、”ラベルにテキストを表示したり、テキストに何かしたり、UIの更新”といった振る舞いになります。この振る舞いを一度定義すると、Rxはいつでもユーザーが入力した新しい文字を検出し、振る舞いを再度適応します。UIがデータモデルと同期しなくなることはもうなくなります。
手軽で、シンプルで、線形的です。
もう少し複雑な例にしてみましょう。振る舞いを定義します。”ユーザーがTable Viewをスクロールし、Table Viewの一番下までいったら、振る舞いを実行します”。ここでいう振る舞いは、”サーバーからさらに20個データをロードし、すでにスクリーンに表示されているリストに追加する”(手軽で、線形的な振る舞い)ということです。ユーザーが画面下までスクロールすると、アイテムが追加されます(そして、追加されたアイテムの一番下まで到達します)。まったく同じ振る舞いが(アイテムがまだあれば)自動で実行されます。これがコードのことを考え始めるのに一番シンプルな方法です。
Rxの関数型プログラミング的な側面 (06:30)
それでは関数型プログラミング的な側面についてみてみましょう。おそらく、Observableはすごいです。
3つ例を出します。Observable型がそれぞれにあります。Text Fieldについては、テキストがあって、 Observable<String>
を用いています。テキストに動作させるなら、ObservableなStringはこの先もStringを保持します。データとテキストをObservableにラップします。このTable View Controllerの例では、Stringの配列で、Observable<Array<...>>
です。スクロールの例では、データはありません。Observable<Void>
です。ユーザーが一番下までスクロールしたというイベントにのみ関心があるだけです。
この3つの例は、同じクラスで動作しています。(可能な限り)同じクラスのメソッドが使えるということです。(ArrayやString、Numberなどの)データ型から開放され、Observableで行えることを基に、全体のワークフローの振る舞いを定義することを考えることができます。ロジックはファイルをコピー&ペーストすることで簡単にやりとりでき、フレームワークに抽出することもできます。特定のプロジェクトで作業したい特定のデータ型に集中することができます。
どんなRx Presenterでも最初にとおらなければならない古典的な例をちょっとみてみましょう。 Text Fieldにクエリを入力し、GitHubで一致するレポジトリを検索できるアプリです。
これをObservableを使ってどうやって作るのか見てみましょう。画面上にはText Fieldがあります。Observable<String>
を、つまり、新しい文字が入力される度に、Stringを受け取ります。filter
というメソッドがObservableクラスにあり、不要な値を取り除くことができます。好ましくないものをフィルターします。例えば、クエリが3文字以下のときは検索したくありません。なぜならたくさんのいらない結果が返ってくるからです。これはObservableにあるメソッドです。これの良いところは、Observableもリターンされることです。結果をフィルターして、さらにその返り値に対して別のObservableクラスのメソッドを呼ぶことができます。メソッドチェーンが次々に行えます。debounce
を使ってみましょう。
Debounce
はObservableのメソッドで、時間的にほぼ同時に行われたイベントを検出し、その最後の一つを取ります。ユーザーがレポジトリの名前を部分的に素早く入力しても、少し時間をとってから、最後のイベントを取得します。次々に呼びたいので、これをチェーンします。
Observableでmap
をコールします。map
はラップしたデータ型を何かに変換できます。この場合は、とてもシンプルです。(検索クエリである)Stringを取って、NSURLRequest(…)
のmapを作ります。すでにマッチしている全ての入力した文字それぞれについて、通信する準備ができたリクエストがあります。
flatMap
はネットワークリクエストを作成し、通信が完了するまで待機し、結果を返します。その結果を元に次に進めます。 NSData
を返します。そして再び map
の中で、今度は NSJSONSerialization
を使って NSData
を Array<AnyObject>
に変換します。そしてもう一回、mapを使って、 AnyObject
を Repo(…)
に変換します。
これはこのアプリのワークフローです( スライド14の上参照 )。Text Fieldへの入力から、その入力のバリデーション、ネットワーク、データ変換を、Table Viewにバインドしていて、リポジトリリストが得られるところまで処理が通過します。おそらくディスク上にあるRealmに(または違う方法で)保存します。とても線形的でいいですね。ある処理がなくなったとしても、シーケンスで起きていることに関して混乱することはありません。データソースプロトコルはありません。デリゲートメソッドもありません。物事が順番に、次から次へと起こり、とても線形なのです。
IBOutletをqueryというText Fieldにつなぐところから始めます。ここで使うRxはStringを渡してくれるObservableになります。
query.rx_text
.filter {string in
return string.characters.count > 3
}
.debounce(0.51, scheduler: MainScheduler.instance)
.map {string in
let apiURL = NSURL(string: "https://api.github.com/q?=" + string)!
return NSURLRequest(URL: apiURL)
}
.flatMapLatest { request in
return NSURLSession.sharedSession().rx_data(request)
}
.map { data —> Array<AnyObject> in
let json = try NSJSONSerialization.JSONObjectWithData(data, options: [])
return json as! Array<AnyObject>
}
.map {object in
return Repo(object: object)
}
.bindTo(tableView.rx_itemsWithCellIdentifier("Cell"))
filter
を呼んで、不要なStringを取り除くコードを書きます。それから、debounce
を呼び、ひとつのグループにまとめるイベントの時間間隔を決めます。map
を呼んで得られたStringをURLにしてURLRequestを返します。次に、flatMap
を呼び、 URLSessionを使ってリクエストを実行し、結果となるデータを得ます。次の map
は (いつも通り)NSJSONSerialization
を使って、NSData
を得ます。その後のmap
はそれぞれのAnyObject
をとり、定義したRepoオブジェクトに変換します。一番最後にメソッドチェーンで、.bindTo
を呼び、CellIdentifierによって、リポジトリのリストがTable Viewと接続することを定義します。
( 見ての通り、これは簡単で短いコードですが、いくつか考慮しています 。)みなさんには15分の(もしくはもっと)Rx経験があります。あなたはこのコードが何をしているかすでにわかるでしょう。とてもシーケンス的なものであり、実行される順序については決して混乱することはありません。あなたがチームの新メンバーであれば、このコードが何をしているのか分かるでしょう。今から6か月後にこのコードを見ても、これが何の処理をするのかわかります。
お互いに関係のないオブジェクトでメソッドを呼び出すのではなく、メソッドチェーンで呼び出すことです。それぞれは、何か入力を期待し、何かを出力します。そして、一度コンパイルすれば、 手をしっかりと握り合っています 。逆にコードにバラバラに導入することはとても難しくなるのでで、コンパイルが通るならスムーズに実行されることが保証されます。
F&R App Architecture (14:44)
これがiOSのアプリにどの関わってくるのでしょう?
(もともとリアクティブでも関数型プログラミング的でもない)View Controllerを表示する、より複雑な例を見てみましょう。Table Viewにリポジトリリストを表示しているNavigation Controllerがあって、リポジトリを手動で追加できるモーダルView Controllerがあります。ユーザーはキーボードを使ってすべてのデータを入力し、完了を押す必要があります。
通常の方法はデリゲートプロトコルを実装することです。コントローラーの1つがもう1つのコントローラーで呼び出すメソッドを持つプロトコルを定義します。さっきの話を思い出すと、ちょっと複雑になります。しかし、これについてすべてを忘れましょう。
ユニバーサルクラスは、すべてのクラス間でユニバーサル通信を可能にするデータ型です。これはObservableです。AddRepoViewControllerに、ユーザーが操作を完了するたびに値を出力するObservableがあれば、とてもシンプルなものになります。
ソースコードを見てみましょう。まず、バーアイテム(右上の「+」ボタン)から始めましょう。 Rxでは、タップは基本的にユーザーが「+」ボタンをタップするたびに発生するObservableです。
addBarItem.rx_tap
.debounce(0.5, scheduler: MainScheduler.instance)
.flatMapFirst {[weak self] _ —> Observable<Repo> in
let addVC = AddRepoViewController()
self?.presentViewController(addVC, animated: true, completion: nil)
return addVC.newRepo.asObservable()
}
.doOn {_ in
self.dismissViewControllerAnimated(true, completion: nil)
}
.subscribeNext {repo in
repos.value.append(repo)
}
repos.asObservable()
.bindTo(tableView.rx_itemsWithCellIdentifier("Cell"))
ダブルタップを防止するdebounce
を再び使います。ユーザーが「+」ボタンで数回すばやくタップしても、画面上にいくつもView Controllerを開くのではなく、Rxが一度だけView Controllerを開きます。
それからflatMap
を使います。前の例では、少し作業をしてから完了するまで待機していました。これはまさに私がモーダル遷移したView Controllerを使ってやりたいことです。View Controllerを表示し、プロパティ経由で公開されたObservableがレポジトリを返すまで待ってください。次に、View Controllerをdismissします。チェーンの最後の呼び出しは、データで何かをします。この場合、Observableが返したリポジトリを、リポジトリリストに追加します。ロジック全体を完成させるために、リポジトリリストをTable Viewにバインドします。
駆け足で、MVVMを説明します(どういう意味かは以前議論が以前ありました。忘れてください)。いくつかView Conrollerがあり、データを駆動するいくつかのモデルがあります。このモデルはすべてのRxのコード(すべてのロジック、すべての呼び出し)を配置する場所です。チェーンの最後の行では、「最後にデータを持っているので、UIで何かする」 ということを明確にしてView Controllerに入れます。
ロジックをバインディングからUIに分離するのは簡単です。ロジックはView Modelにしかないので、View Modelをテストするテストを作成し、テストでView Controllerや他の意味ないところをインスタンス化することはありません。分かれていると感じられるようになるのは簡単です。
RxSwift (18:56)
- RxSwiftは同期的に書ける非同期処理のフレームワークです。
- 関数型プログラミングの側面があります。それがこの非同期イベント(変換や、その実行)を可能にします。
- 良いアーキテクチャになろうとがんばっています。
iOS開発ととても相性がいいです(もちろんですよね?)
参考資料 & 謝辞 (19:46)
- Rxは色んなプラットフォーム上の色んな言語で実装されているAPIなので、ReactiveX.io はSwiftやJava、JS、Scalaでの使い方を紹介しています。
- RxSwift.org
- rx_marin.com には私が書いた記事があって、Rxを始めるのに役に立つと思います。
年齢順に。Ash Furrow、本当にすばらしいひらめきをありがとう。Jen Ravensは基礎を教えてくれました。Florent Pilletは私がプログラミングを始めたころのコードを直してくれました。 Junior Bontognaliは友人で、すごいやつで、(Rxに対しても)協力的です。そして Krunoslav Zaherへ。RxSwiftを作った方です。
ここにいるのはNatashaのおかげです。ニューヨークに呼んでくれたのは彼女です。Realmがここに連れてきてくれました❤️。もしRealmに興味があるなら、Realmは新しいメンバーを募集中です!。
新しく記事が更新されたらメールにてお知らせします。