こんにちは、オープンエイトでiOSアプリエンジニアをしている常盤です。 通勤時間に小説を読むようにしたらQOLが爆上がりしました。
さて、私は約半年前にオープンエイトにジョインし、その時初めてSwiftに触れました。 弊社のサービスである女性向けおでかけ動画メディア、Letronc。iOSアプリではRxSwiftが使われています。 初めて触るSwiftという言語に加え、Rxという概念を理解するまでは艱難辛苦の日々でした。。。 そこでRx初心者に向けて、どうやって概念を理解してコードを書けるようになったかポイントを伝授しようと思います。
Rx is 何
Rx(Reactive X)とは、「オブザーバパターン」「イテレータパターン」「関数型プログラミング」の概念を実装している拡張ライブラリです。
Rxを導入するメリットは、「値の変化を検知できる」「非同期の処理を簡潔に書ける」ということに尽きると思います。 値の変化というのは変数値の変化やUIの変化も含まれます。 例えばボタンをタッチする、という動作もボタンのステータスが変わったと捉えることができRxを使って記述することができます。
Rxの考え方
私がRxで躓いたポイントは3点あると考えています。
1.Rx特有のキーワードがそれぞれどのような働きをしているか理解できていない
2.メソッドチェーンのような書き方で値の流れが理解できていない
3.値の変化に応じて発火するイベントが理解できていない
...そう...何も理解できていないからである!
しかし理解できていない対象が分かっていれば対処は可能です。
1.に関しては基本のキーワードを押さえていき、 2.3.は実装例から紐解いて行こうと思います。
基本のキーワード
SubjectとRelay
すごく簡単に言うと、イベントの検知に加えイベントの発生もできる便利なラッパークラスです。
代表的としてよく使われる4種類は以下です。
・PublishSubject
・BehaviorSubject
・PublishRelay
・BehaviorRelay
それぞれ特徴が異なるのですが割愛します。本記事のコードではBehaviorRelay
を使用します。
ObservableとObserver
Observable
は監視可能なものを表すクラスで通知を行うことができます。
変数xがObservable
でラップされているならば、変数xの値が変化する毎にObserver
へ通知されます。
Rxではストリーム(stream)とも表現されています。
Observer
は監視を行う側です。Observer
はObservable
から通知されるあらゆる値に反応することができます。
subscribe()
subscribe()はメソッドで、監視する側の働きを持ちます。
Observable
から通知を受け取ったらどのような振る舞いをするかを定義します。これをRxでは購読と表現します。
引数であるonNext
がとるクロージャー内でやりたい処理を記述します。
DisposeBag
一度購読をし始めるといつまで購読し続けるのか?メモリに残ったままにならないのか?そんな疑問が湧くかと思います。 DisposeBagは購読の破棄をうまいことやってくれてメモリリークを回避するためのクラスです。 イメージとしては、購読を定義した際に返ってくる破棄可能なインスタンスをDisposeBagインスタンスに入れることで、そのDisposeBagインスタンス自身が破棄されると同時に購読も破棄してくれます。 購読はビューが破棄(deinit)されるタイミングで一緒に破棄したいことが多いので、ビュー、またはビューコントローラーのインスタンス変数として定義することが多いです。
実際に使ってみた
最低限の実装でも良いのでコードを書いてその働きを追うことが理解への第一歩かと思います。
実装例として数字カウントアプリを考えてみましょう。
ボタンを押すと、ラベルに表示される数字が+1されるというアプリです。これをRxを使って実装していきます。
ここでRxが適用できる処理はどの部分でしょうか?以下の2つの処理はRxを用いて記述することができます。
1.カウントボタンを押したら、変数の値を変更する
2.変数の値が変化したら、変化後の値をビューに表示する
iOSアプリでよく見る形のMVVMアーキテクチャに当てはめると、実装のイメージは下記図の通りです。
それではコードに落とし込んでいきましょう。 今回MVVMアーキテクチャは主題から外れる、かつコードは非常にシンプルなので、処理はすべてビューに集めています。
// SomeViewController.swift import UIKit import RxSwift import RxCocoa class SomeViewController: UIViewController { @IBOutlet weak var button: UIButton! @IBOutlet weak var label: UILabel! private let count: BehaviorRelay<Int> = BehaviorRelay(value: 0) private let disposeBag: DisposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() bindButtonToValue() bindCountToText() } // 1.の処理 private func bindButtonToValue() { button.rx.tap .subscribe(onNext: { [weak self] _ in self?.increment()}) .disposed(by: disposeBag) } private func increment() { count.accept(count.value + 1) } // 2.の処理 private func bindCountToText() { count.asObservable() .subscribe(onNext: { [weak self] count in self?.label.text = String(count) }) .disposed(by: disposeBag) } }
実際の動きは以下のようになります。これ以上ないくらいシンプルです。
上の実装例からRxの書き方を紐解いていきます。
メソッドチェーンのような直感的な書き方
Rxはメソッドチェーンのように、メソッドの結果に対して更にメソッドを呼ぶことができます。 この形式はRx特有の特徴ではなく関数型プログラミングではよく見られます。 意識して頂きたいのは各メソッドの返り値です。返り値の型によってチェーンできるメソッドが異なります。
bindCountToText()を見てみましょう。これは典型的な変数購読の書き方です。 返り値をコメントアウトで表してみます。
count.asObservable() // Observable<Int> .subscribe(onNext: { [weak self] count in self?.label.text = String(count) }) // Disposable .disposed(by: disposeBag) // 返り値なし
asObservable()メソッドは、返り値としてObservable
を得ることができます。
基礎キーワードで出てきたObservable
、繰り返しになりますがこれは監視可能なクラスです。つまり変数countの値の変化を監視している状態になります。
監視しているだけじゃ何もおきないので、購読をします。値が変化したらやりたい処理をsubscribe()メソッドで定義します。ここでやりたいのはラベルに変化した値をいれることです。
subscribeメソッドの返り値はDisposable
インスタンス。なにか見覚えがありますね?ここで、基礎キーワードに出てきたDisposeBagによる購読の破棄を行います。
disposed(by:)メソッドの引数にインスタンス変数として定義したdisposeBagを与えることで、SomeViewControllerが破棄(deinit)されるとdisposeBag自身も含め購読も破棄してくれます。disposed(by:)メソッドを呼び出したらすぐに購読が破棄されることはありません。
値の変化でイベントが発生
画面上ではボタンを押すと表示される数字がカウントアップするシンプルなものなのでRxを使う旨味がないように思われるかもしれません。
しかし実際のアプリではアーキテクチャに当てはめたりと複雑性は増します。
上記の実装1、2のように分けて考えることができれば、値の変化を検知しイベントが発生する
ことが捉えやすくなるかと思います。
そのイベントに応じて、やりたい処理を簡単に記述できるのがRxの便利なところです。
特に非同期処理(例えば、ネットワークを介したAPI処理)において、従来ではコールバックとして定義していた箇所がRxを適用することによりコールバック地獄から解放され、スッキリとしたコードに収めることができます。
1ステップ先へ
bindCountToText()は以下のようにも書き換えることができます。
count.asObservable() .map{ String($0) } .bind(to: label.rx.text) .disposed(by: disposeBag)
今回取り上げてはいませんが、mapというオペレーターを使ってObservable
から流れてきた値を加工しています。
そして加工した値をbind(to:)メソッドでラベルのテキストに直接バインドしています。
このようにRxではObservable
とViewを直接つないだりすることもできます。(Swift標準のUIKitをRxで扱うにはRxCocoaというライブラリが必要です。そのためRxSwiftと一緒に使われることが多い。)
まとめ
今回、RxSwiftについてまとめました。少しでも概念がイメージできたなら嬉しいです。
Rxは学習コストが高い
やらパット見わかりにくい
と言われておりますが、慣れていくとすごく便利なライブラリだと感じます。
私はもうRx無しでは生きられない身体になりました。
難しく考えずにほんとにシンプルなことから実装してみるのが一番の近道かもしれません。
ぜひ自分のモノにしてRxのパワーを感じて欲しいです。