Edited at
RxSwiftDay 23

ViewControllerをObservableとして考える

More than 1 year has passed since last update.

昨日は同僚の @ukitaka 君がスレッド周りの話についてわかりやすく書いてくれました。

一転して僕の話はわかりにくい内容になってしまった。すみません😔


これはなに

ViewControllerの内側だけでなく、外側もObservableとして考えてみると色々捗るのではないでしょうか、という提案です。

幾つか自分の中で答えが出せていない部分もあるので、その点はご容赦ください。すみません。


Observableとイベント

RxSwiftを取り入れたプロジェクトでは、様々なイベントをObservableとして表現し、組み合わせ、プログラムを書いていくことになります。

例えば、ユーザがボタンをタップしたり button.rx.tap

APIRequestを投げて結果が帰ってきたり urlSession.rx.send(apiRequest)

そういった様々なイベントを組み合わせてプログラムを書きます。

「ユーザがボタンをタップしたらAPIを叩いてその結果をTextViewに表示する」というのは

button.rx.tap

.flatMapFirst { urlSession.rx.send(apiRequest) }
.map(String.init)
.observeOn(Mainscheduler.instance)
.bindTo(textView.rx.text)
.addDisposableTo(disposeBag)

みたいな書き方になると思います。

そして、Observableを使うことの興味関心の多くは、ViewControllerの内側に向いている気がするな、と個人的に思っていたりします。

(もちろん、色々な宗教があってロジックはどこに書くんだみたいな話題があることは存じていますが、それも含めて。)


ViewControllerとタスク

ViewControllerはしばしばユーザの目的、タスクを表現するコンポーネントとして利用されています。

例えばUIImagePickerControllerは、写真を選択するというタスクであり、

例えばUIAlertControllerは、複数の選択肢から一つを選択するというタスクです。

多くの人はログイン画面を作ったことがあると思います。もちろんこれもログインする、というタスクですね。

これらのタスクは、ViewControllerの外側、例えば呼び出し元のViewControllerやアプリのステートに作用します。なので、多くの場合はDelegateやHandlerを使うのではないでしょうか。

ViewControllerとタスクの関係は、次のような性質があると考えています。

1. タスクが終了するとき、ViewControllerは画面から表示されなくなる。

2. タスクは結果を伴うことがある。

あれ、これってもしかして…


Observableとタスク

タスクをObservableに当てはめて考えてみると、

1. タスクの終了はCompleted

2. タスクの結果はNext

として表現できます。

例えばUIImagePickerControllerでキャンセルボタンを押した場合は、Completedだけが流れてくる、

写真を選択した場合は、Next(Picture)Completedが流れてくるイメージです。

実際の実装としては、キャンセルや写真の選択のイベントは、UIImagePickerControllerDelegateに宣言されているので、RxDelegateを実装して、Observableを合成すればうまくいきます。

UIImagePickerControllerのRx実装は、本家のRxExampleを拝借しましょう。

extension Reactive where Base: UIImagePickerController {

public var task: Observable<[String: AnyObject]> {
return Observable
.of( // キャンセルと写真の選択をOptionalで表現して型を揃えます
didFinishPickingMediaWithInfo.map { Optional.some($0) },
didCancel.map { Optional.none }
)
.merge()
.take(1) // 最初の1回目を採用します
.flatMap { Observable.from($0) } // .someだけ.nextとして流します
}
}

UIImagePickerControllerのタスクを、Observableとして表現することができました。

タスクは色々なイベントを組み合わせて実現しているので、Observableで表現できるのは至極当たり前ですが、実際に書いてみると色々腑に落ちてきます。

さて、rx.taskを使うと、「ユーザがボタンをタップしたらImagePickerを表示して選択された画像をImageViewに表示する」というのが

button.rx.tap

.flatMapFirst { [unowned self] in
let imagePicker = UIImagePickerController()
return imagePicker.rx.task
.do(
onCompleted: { imagePicker.dismiss(animated: true, completion: nil) }
onSubscribe: { self.present(imagePicker, animated: true, completion: nil) }
)
}
.map { $0[UIImagePickerControllerOriginalImage] as? UIImage }
.bindTo(imageView.rx.image)
.addDisposableTo(disposeBag)

このように記述できるようになりました。

Taskを持つViewControllerをprotocolとして表現して、flatMapFirstの中身をUIViewControllerにrx.presentの実装として移動させると、

button.rx.tap

.flatMapFirst { [unowned self] in
self.rx.present(UIImagePickerController())
}
.map { $0[UIImagePickerControllerOriginalImage] as? UIImage }
.bindTo(imageView.rx.image)
.addDisposableTo(disposeBag)

このように記述することができるようになります。

rx.presentが画面遷移について担保しているので、UIImagePickerControllerは、タスクについてのみ考えておけば良いですね。


 問題点とか

Swift3.0ではextension Reactive where Base: XXXに特定の関数を持つ型、というのは表現できませんので、rx.presentの実現のためには、何かしらのWorkaroundは必要です。

僕が今携わっているProjectでは、

protocol ObservableViewController {

associatedtype Result
func result() -> Observable<Result>
}

という型を作って、これを実装したViewControllerを引数に取るrx.presentを書いていますが、以下の2点の問題があってどうしようかな~と考えているところです。

1. resultがObservableなのにReactive以下に置くことができない。

2. クラスに対してResultが一意に決まってしまう。UIAlertControllerをPrompt、またはConfirmとしてのみ使うことができなくなってしまう。かと言ってサブクラスを作るのは違う気がする…

Task型をViewControllerより外側に置くのは良いかもしれない、どうするかな~

もう一つは、Completedを流すということは、ViewControllerは再利用するべきではないので、再利用し得るシチュエーションについて、考察を深める余地があります。

例えばUIImagePickerControllerから更に次の画面をPushする場合など。次の画面でキャンセルすると、UIImagePickerControllerに戻ってきますが、rx.taskはsubscribeしなおさないとCompletedが流れてしまったあとです。

再利用するということは、正確にはタスクは完了していないので、Completedを流すべきではない、のかもしれない。うーん🤔


 まとめ

ViewControllerに含まれるタスクと、画面遷移のロジックを分離して、タスクをObservableで表現することを考えてみました。

画面遷移を伴うプログラムを、Observableに記述することができるようになります。やったー

タスクを持つViewControllerを表現するために、どういう型を使うべきか、というのはPlaygroundと戯れてますが、まだ見いだせていません。