ReactorKit とは
ReactorKit は、Swift アプリケーション開発向けのフレームワークのひとつです。RxSwift と Flux(単方向フロー)とを組み合わせた、軽量なフレームワークとなっています。
ReactorKit の設計の目標は以下の3点です。
- Testability : テスト容易性、View とロジックの分離により実現する
- Start Small : アプリの一部だけでも適用できる
- Less Typing : コードを複雑にしない
この記事では、特に Testability に注目して ReactorKit を見てみます。
構成
ReactorKit の構成は以下のようになっています。
View
と Reactor
とのふたつで成り立っています。両者間のデータの受け渡しのためのオブジェクトとして、Action
と State
があります。
View と Reactor の実装
View
と Reactor
は、それぞれ次のような責務を持ちます。
-
View
の責務 (1)- UI 操作を受け取る →
Action
としてReactor
に渡す
- UI 操作を受け取る →
-
Reactor
の責務-
Action
を受け取る → 処理を行いState
を生成する
-
-
View
の責務 (2)-
State
を受け取る → UI に表示する
-
iOS アプリでは、これらの責務を UIViewController
が抱え込みがちです。それを View
と Reactor
に分離しています。
-
UIViewController
にはView
の責務だけを持たせる -
Reactor
は別オブジェクトとして分離(なお、Reactor
は UIKit は使わない)
これにより、Testability が向上します。特に View
の Testability について後ほど触れます。
View プロトコル
View
プロトコルには bind
メソッドが定義されています。
class MyViewController: View {
func bind(reactor: MyReactor) {
// ここで Reactor とのバインディングを行う
// RxSwift を活用
}
}
Reactor プロトコル
Reactor
プロトコルには Action
と State
が定義されています。
class MyReactor: Reactor {
enum Action {
case myAction
case myActionWithArg(Int)
}
struct State {
var myValue1 = ""
var myValue2 = false
}
}
View の実装 (1)
View
の責務 (1) を実装するには、UI 操作を Reactor
が持つ Action
にバインドすればよいです。
func bind(reactor: MyReactor) {
myButton.rx.tap
.map { Reactor.Action.myAction }
.bind(to: reactor.action)
.disposed(by: self.disposeBag)
なお、バインド先の reactor.action
は ActionSubject
です(RxSwift が持つ Subject
の ReactorKit による派生)。
View の実装 (2)
View
の責務 (2) を実装するには、Reactor
が持つ State
を UI 表示にバインドすればよいです。
func bind(reactor: MyReactor) {
reactor.state
.map { $0.myValue1 }
.bind(to: myLabel.rx.text)
.disposed(by: self.disposeBag)
なお、バインド元の reactor.state
は RxSwift の Observable
です。
Reactor の実装
Reactor
の責務の実装は、mutate()
と reduce()
の二段階に分けて実装します。
-
mutate()
:Action
を受け取ってObservable<Mutation>
を返す -
reduce()
:Mutation
と前のState
を受け取ってState
を返す
Mutation
は Reactor
内部で使う、状態変化をあらわすオブジェクトです。
mutate()
-
Action
を受け取ってObservable<Mutation>
を返す - 具体的な処理(Web API 実行、データベースアクセス、など)を行う
Observable
を生成する
実際に処理を行うかどうかの判断などは Reactor
の役目です。これにより、View
側では Action
を生成するかどうかを判断しないため、View
の実装がバインディングだけですみます。
reduce()
-
Mutation
と前のState
を受け取ってState
を返す - 処理の結果と前の状態を考慮して次の状態を決めるのが役目
前の状態は Reactor
が持っています。これにより、View
側では前の状態を考慮するといった処理が不要になるため、View
の実装がバインディングだけですみます。
View の単体テスト
さて、View
の Testability を考えましょう。
そもそも一般に、単体テストでは何をテストするべきでしょうか? ここでは、「そのオブジェクトの責務を果たしているか」をテストするべき、と考えることにします。
そこで、以下のふたつについてテストすることを考えます。
-
View
の責務 (1) : UI 操作 →Action
-
View
の責務 (2) :State
→ UI 表示
View のテスト (1) : Action
責務 (1) のテストでは、コードで UI 操作を発生させて、その結果として、Reactor
に Action
オブジェクトが渡されたことを確認します。
コードで UI 操作を発生させるには、UIControl
の sendActions
メソッドを使えばよいです。
Reactor
に Action
オブジェクトが渡されたことを確認するには、Reactor
のスタブを用意すればよいです。実は ReactorKit ではスタブがすでに用意されています。
これにより、以下のようにテストが書けます。
func testMyAction() {
let reactor = MyReactor()
reactor.stub.isEnabled = true
let view = MyView()
view.reactor = reactor
view.myButton.sendActions(for .touchUpInside)
XCTAssertEqual(reactor.stub.actions.last, .myAction)
}
View のテスト (2) : State
責務 (2) のテストでは、コードで Reactor
の State
を変化させて、その結果として、UI 表示が変更されたことを確認します。
やはり Reactor
のスタブを活用して、コードで Reactor の State 変化を起こすことができます。
これにより、以下のようにテストが書けます。
func testMyAction() {
let reactor = MyReactor()
reactor.stub.isEnabled = true
let view = MyView()
view.reactor = reactor
reactor.stub.state.value = MyReactor.State(myValue1: "abc")
XCTAssertEqual(view.myLabel.text, "abc")
}
View のテストまとめ
- 何がテストできたのか? :
View
が責務を果たしていること -
View
の実装でいえば、バインディングがちゃんとできていること
ところで実のところ、そもそもこの単体テストは必要なのかという疑問も生じます。ReactorKit を活用してシンプルなオブジェクトになった結果、View
はバインディングしかしていないわけですから、単純すぎてテスト不要というレベルになっているとも言えます。
それでも単体テストを書いておくほうがメリットがあるというのが私見です。ただ、実際にテストコードを書くかどうかはケースバイケースかもしれません。
Reactor の単体テスト(概略)
Reactor
のテストについても触れておきます。ただ、これは ReactorKit の話というよりも一般論になってしまうので、概略にとどめます。
先ほどと同様、「そのオブジェクトの責務を果たしているか」をテストすることを考えます。つまり、以下についてテストすることを考えます。
-
Reactor
の責務 :Action
→State
ここで、Reactor
は View
への依存がないため、View
とは独立してテスト可能であるというのが ReactorKit の恩恵です。
実際にテストを書くには、コードで Action
を発生させて、State
変更を確認します。
しかし、Reactor
の中で Web API 実行、データベースアクセス、などがあると単体テストはしづらいです。そこで、それらの処理を Service として Reactor
の外に切り出しておく必要があります。
そのうえで、Service をスタブ化することで Reactor
が単体テスト可能になります。
まとめ
- 複数の責務を持ちがちな
UIViewController
について、ReactorKit でView
とReactor
に責務を分離することができます。 - 適切な分離により、
View
やReactor
がテスト可能になります。
※ 本記事は、Mobile Act OSAKA #8 で登壇して発表した内容を記事の形にしたものです。
補足
RxSwift のバインディングについては、別記事を参考にしてもらえればと思います。→ RxSwift 再入門