ReactorKitを学ぶシリーズ
- ReactorKit(Flux + Reactive Programming)を学ぶ1 入門編 ← 今ここ
- ReactorKit(Flux + Reactive Programming)を学ぶ2 基礎編
- ReactorKit(Flux + Reactive Programming)を学ぶ3 実践編
更新履歴
2018-03-28
-
v0.4.5からv1.1.0に内容を更新
- StoryboardViewプロトコルについて追記
- サンプルのCounterの解説部分を最新の内容に合わせて更新
2017-06-10
- 初稿
この記事で学べること
- ReactorKitの概要
- ReactorKitの最低限の実装
- ReactorKit/Examples/Counter/の解説
※ この記事は、ReactorKit v1.1.0を元に書いています。
ReactorKitとは
ReactorKitは、リアクティブで単方向ストリームのアーキテクチャを構築するためのフレームワークです。
アーキテクチャ名は、The Reactive Architecture
です。ReactorKitで構築できるアーキテクチャがThe Reactive Architecture
ということなのですが、便宜上、ReactorKit
== The Reactive Architecture
ということで説明します。
2017/1/7に公開された新しめのアーキテクチャ & フレームワークです。詳しくは後述しますが、FluxとReactive Programming(RxSwift)を組み合わせて構築するアーキテクチャで、新しい概念や技術で1から作られたものではありませんので、現段階でも実用に耐えられる作りとなっています。実例として現在App Storeで公開されているの Dribbbleアプリは、ReactorKitを使用しており、実装はGitHubに公開されています。
ReactorKitは、2018/3/28の時点でGitHubのStarは951です。
開発者の方は、@devxoulさんで、様々なOSSを開発されている方です。
- Then ⭐️ 1,666
- URLNavigator ⭐️ 1,159
- Toaster ⭐️ 756
- SwiftyImage ⭐️ 612
- RxKeyboard ⭐️ 444
何を解決したいのか
個人的にですが、各アーキテクチャには次のような問題点を感じています。
- MVC
- MVVM
- アプリの規模が大きくなるとViewModelの初期化、Observableストリームが複雑になる
-
Clean Architecture、VIPER
- 正しくアーキテクチャを構築するための学習コスト
- 抽象化によるラビオリコードの問題(リンク先のQ&Aの部分)
詳しくは、iOSアプリのアーキテクチャについて考えるをご覧ください。
ReactorKitは、これらの問題を 良いバランスで解決 できそうだと感じています。
一つだけ強調しておきたいことは、この記事は 各アーキテクチャの優劣について話しているわけではありません。 どのアーキテクチャを選択するかは、各プロジェクトの要件や規模によりますし、実装者の好みもあると思います。結局は何か処理してそれを画面に表示するということは変わらず、それをどういう枠組みで整理するかということですしね。
前提知識
ReactorKitは、RxSwiftに依存しています。単方向ストリームの構築部分もそうなのですが、使う側が実装するロジック部分もRxSwiftが必須になっています。
RxSwiftはとても学習コストが高いのですが、学ぶ価値は大いにあります。
RxSwiftについては次の資料が参考になると思います。
- 概要
- 公式ドキュメント
- 実装
-
オブザーバーパターンから始めるRxSwift入門
- 長いですがシリーズ通して読めばある程度わかるようになります。
-
RxSwift コードリーディングの勘所@社内RxSwift勉強会
- RxSwift v2.5.0での解説ですが、source/subscribe/run/on/Sinkなど内部実装を読み解くヒントになります。
-
オブザーバーパターンから始めるRxSwift入門
ReactorKitの特徴
ReactorKitの特徴は、ReactorKitがアーキテクチャの枠組みを用意してくれるので、ReactorKitのルールを守り実装を行えばアーキテクチャを導入できるという点です。アーキテクチャの枠組みが用意され、何をどこに実装するかが明確なのでアーキテクチャを正しく構築するための実装上の迷いが軽減されます。
メインの実装は、ReactorプロトコルとViewプロトコルの2つで、非常に軽量なフレームワークです。
※ v0.6.0で、StoryboardからUIViewControllerを生成しているView用に使用するStoryboardViewプロトコルが追加されました。
ReactorKitの実装
ReactorKitは、FluxとReactive Programming(RxSwift)を組み合わせた、アクティブで単方向のストリームを実現しています。
Actionとは
- Actionはユーザの操作を表します。
- ViewがActionを発行します。
enum Action {
case refresh // 更新
case toggleEditing // 編集モードを切り替える
case toggleTaskDone(IndexPath) // タスクの完了を切り替える
case deleteTask(IndexPath) // タスクを削除する
case moveTask(IndexPath, IndexPath) // タスクを移動する
}
Stateとは
- StateはViewの現在の状態を表し、具体的な値を保持します。
- ViewはStateの状態からUIを更新します。
struct State {
var isEditing: Bool // 編集モードか
var sections: [TaskListSection] // タスク(モデルデータ)の配列
}
Reactorとは
- Reactorは、Viewの状態を管理するUIに依存しないレイヤー(層)です。そのためReactorではUIKitをimportしてはいけません。
- 最も重要な役割は、Viewの制御フローを分離することで、すべてのViewには対応するReactorがあり、すべてのビジネスロジックをReactorに移譲します。
- ReactorはUIに依存しないため簡単にテスト可能です。
- Reactorを実装するためのReactorプロトコルが定義されています。
Viewとは
- Viewはデータを表示するところです。
- テーブルビューを例にすると、UITableViewControllerやUITableViewCellはViewにあたります。
- Viewはユーザのインプット(テキスト入力やボタンをタップしたなど)でAction発行し、Reactorの単方向ストリームにバインドします。
- ViewはStateとUIコンポーネントをバインドし、UIを更新します。
- Viewを実装するためのViewプロトコルが定義されています。
単方向ストリームとは
- 単方向ストリームとは、
View → Action → State → View
の流れを指します。- ViewはユーザのインプットをきっかけにActionを発行する
- ReactorはActionを受けてStateを更新する
- ViewはStateとUIをバインドし、Stateの変化に応じてUIを更新する
Reactorの処理
Reactorは、Viewから発行されたActionから最終的にはStateを更新するのですが、その中間の状態としてMutationがあります。
Mutationとは
- MutationはStateの更新内容を表します。
- MutationはReactor内部でのみ使用されます。
enum Mutation {
case toggleEditing // 編集モードを切り替える
case setSections([TaskListSection]) // タスクをセットする
case insertSectionItem(IndexPath, TaskListSection.Item) // タスクを挿入する
case updateSectionItem(IndexPath, TaskListSection.Item) // タスクを更新する
case deleteSectionItem(IndexPath) // タスクを削除する
case moveSectionItem(IndexPath, IndexPath) // タスクを移動する
}
Reactor内のデータの流れ
- Reactor内では、
Action → Mutation → State
とデータの変更を行います。 - Reactorには各状態を変更するメソッドが用意されています。
- ActionをMutationに変更する
mutate(action:)
メソッド - MutationからStateを生成する
reduce(state:mutation:)
メソッド
- ActionをMutationに変更する
Serviceとは
ReactorKitにはビジネスロジックを行うためのServiceレイヤーが設けられています。Serviceレイヤーは必須ではないのと、今回はまずはシンプルなReactorKitの実装解説を目指しているので、Serviceレイヤーについてはまたの機会に解説を行います。
ReactorKitの実装
ReactorKitの実装は、ReactorプロトコルとViewプロトコルに準拠し、Stateストリームを構築することです。
Reactorプロトコルへの準拠
Reactorプロトコルは次の通りです。
- 新たにStateストリームという用語が出ていますが、前述の
単方向ストリーム
==Stateストリーム
になります。各コメントは私の意訳+補足を追加しています。気になる方は原文をご覧ください。
public typealias _Reactor = Reactor
public protocol Reactor: class, AssociatedObjectStore {
associatedtype Action
associatedtype Mutation = Action
associatedtype State
/// Viewからのアクション。ユーザが発行するActionをこのSubjectにバインドします。
/// ActionSubjectはReactorKitが実装しているSubjectで`.next`のみ発行します。
var action: ActionSubject<Action> { get }
/// Stateの初期値です。
var initialState: State { get }
/// 現在のStateです。ストリームによって変更されます。
var currentState: State { get }
/// Stateストリームです。このObservableでStateの変更をObserveできます。
var state: Observable<State> { get }
/// Actionのグローバルな変更を行うことができます。このメソッドは他のObservablesとの結合に使用できます。
/// このメソッドはActionが発行された直後、`mutate(action:)`の直前に呼び出されます。
func transform(action: Observable<Action>) -> Observable<Action>
/// ActionからMutationを確定させます。ここでは非同期タスクなど副作用を伴う処理を実行するのに適しています。
/// このメソッドはActionが発行されるたびに呼び出されます。
func mutate(action: Action) -> Observable<Mutation>
/// Mutationのグローバルな変更を行うことができます。このメソッドは他のObservablesとの変換または結合に使用できます。
/// このメソッドは`mutate(action:)`の直後、`reduce(state:)`の直前に呼び出されます。
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
/// 前回のStateとMutationから新しいStateを生成します。ここでは副作用を起こすべきではありません。
/// このメソッドはMutationが発行されるたびに呼び出されます。
func reduce(state: State, mutation: Mutation) -> State
/// Stateのグローバルな変更を行うことができます。このメソッドでロギングなどの副作用を実行することもできます。
/// このメソッドは`reduce(state:mutation:)`の直後、新しいStateがcurrentStateに適用される直前に呼び出されます。
func transform(state: Observable<State>) -> Observable<State>
}
最低限実装すべきものは次の通りです。
定義 | 説明 | 最適な型 |
---|---|---|
Action | Viewが行うアクション | 列挙型 |
Mutation | Actionに対する変更内容 | 列挙型 |
State | Viewの状態(具体的な値を保持) | 構造体 |
initialState | Stateの初期値 | State型 |
メソッド | 説明 | ルール |
---|---|---|
mutate(action:) | ActionからMutationを生成する | 非同期処理などの副作用を伴う処理が可能 |
reduce(state:) | MutationからStateを生成する | 同期処理のみを行い副作用を伴う処理は行わない |
-
transform
系は少し特殊な処理を受け持ち、複数のReactor間に影響する処理の対応に使用します。これは今回の例では使わないのでまたの機会に解説します。
Viewプロトコルへの準拠
Viewプロトコルは次の通りです。
public typealias _View = View
public protocol View: class, AssociatedObjectStore {
associatedtype Reactor: _Reactor
/// RxSwiftでおなじみのDisposeBagです。
/// 注意点として`reactor`が割り当てられるたびに新しく生成されます。
var disposeBag: DisposeBag { get set }
/// Viewが使用するReactorです。
/// このプロパティに新しいReactorが割り当てられると、`bind(reactor:)`が呼び出されます。
var reactor: Reactor? { get set }
/// ViewとReactor間の各バインディングを記述するところです。
/// `reactor`プロパティが割り当てられると呼び出されます。
/// 注意点として、このメソッドは内部で自動で呼び出されるもので、直接呼び出してはいけません。
func bind(reactor: Reactor)
}
-
disposeBag
を定義し、ViewとReactor間の各バインディングをbind(reactor:)
に実装します。 -
bind(reactor:)
は新しいReactorが割り当てられると内部で呼び出されるため、直接呼び出してはいけません。
StoryboardViewプロトコルについて
- StoryboardからUIViewControllerを生成しているView用に使用するStoryboardViewプロトコルが設けられています。
public protocol StoryboardView: View, _ObjCStoryboardView {
}
- StoryboardViewプロトコルはViewプロトコルを継承しているだけで、追加の実装はありません。
- Viewプロトコルとの違いは内部で呼び出されている
bind(reactor:)
の呼び出しタイミングです。bind(reactor:)
は、reactorがセットされたタイミングで内部的に呼び出されるのですが、StoryboardからUIViewControllerを生成すると、initの段階ではStoryboard上で追加してあるsubViewはまだ生成されていないため、もしもbind(reactor:)
でそれらのsubViewにアクセスするとランタイムエラーが発生します。そのためViewプロトコルしかないときは、subViewの生成タイミングを考慮してreactorのセットを遅らせたりする必要があり非常に煩雑だったのですが、StorybarodViewプロトコルではbind(reactor:)
の呼び出しタイミングがViewDidLoad()
以降のタイミングになるように調整され、reactorのセットタイミングの問題が解消されています。
サンプルアプリCounterを解説
実装はサンプルを見るのが一番わかりやすいので、ReactorKitのExamplesにあるCounterを例に各コードを解説していきます。
ReactorKit/Examples/Counter/README.md
中央にカウントを表示し、 -
で-1、 +
で+1とカウントを増減することができるアプリです。カウントの更新中を表すUIActivityIndicatorViewもカウントの下にあります。
// Viewプロトコルに準拠すると`self.reactor`プロパティが利用可能になります。
// CounterViewControllerはStoryboardから生成されますのでStoryboardViewプロトコルに準拠しています。
final class CounterViewController: UIViewController, StoryboardView {
@IBOutlet var decreaseButton: UIButton!
@IBOutlet var increaseButton: UIButton!
@IBOutlet var valueLabel: UILabel!
@IBOutlet var activityIndicatorView: UIActivityIndicatorView!
var disposeBag = DisposeBag()
func bind(reactor: CounterViewReactor) {
// Action
increaseButton.rx.tap // タップイベント
.map { Reactor.Action.increase } // タップイベントをAction.increaseに変換
.bind(to: reactor.action) // Actionをreactor.actionにバインド
.disposed(by: disposeBag)
decreaseButton.rx.tap // タップイベント
.map { Reactor.Action.decrease } // タップイベントをAction.decreaseに変換
.bind(to: reactor.action) // Actionをreactor.actionにバインド
.disposed(by: disposeBag)
// State
reactor.state.map { $0.value } // カウントの値
.distinctUntilChanged() // カウントの値に変化があるか
.map { "\($0)" } // カウントの値を文字列に変換
.bind(to: valueLabel.rx.text) // カウントの値の文字列をUILabelのtextにバインド
.disposed(by: disposeBag)
reactor.state.map { $0.isLoading } // ロード中か
.distinctUntilChanged() // 値に変化があるか
.bind(to: activityIndicatorView.rx.isAnimating) // ロード中かをUIActivityIndicatorViewのアニメーションにバインド
.disposed(by: disposeBag)
}
}
final class CounterViewReactor: Reactor {
// Viewが行うアクション
enum Action {
case increase
case decrease
}
// Actionに対する変更内容
enum Mutation {
case increaseValue
case decreaseValue
case setLoading(Bool)
}
// Viewの状態(具体的な値を保持)
struct State {
var value: Int
var isLoading: Bool
}
let initialState: State
init() {
self.initialState = State(
value: 0, // カウントする値の初期値は0
isLoading: false
)
}
// ActionからMutationを生成する
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .increase:
return Observable.concat([
Observable.just(Mutation.setLoading(true)),
Observable.just(Mutation.increaseValue).delay(0.5, scheduler: MainScheduler.instance),
Observable.just(Mutation.setLoading(false)),
])
case .decrease:
return Observable.concat([
Observable.just(Mutation.setLoading(true)),
Observable.just(Mutation.decreaseValue).delay(0.5, scheduler: MainScheduler.instance),
Observable.just(Mutation.setLoading(false)),
])
}
}
// MutationからStateを更新する
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case .increaseValue:
state.value += 1
case .decreaseValue:
state.value -= 1
case let .setLoading(isLoading):
state.isLoading = isLoading
}
return state
}
}
実装の流れ
- 必ずしもこの流れで実装するわけではないのですが、Counterを例に実装の流れを解説します。
1. Stateの定義
- Viewの状態(具体的な値を保持)を表すためのStateを定義します。カウントするための値を保持する
value: Int
とロード中を表すvar isLoading: Bool
を定義します。
struct State {
var value: Int
var isLoading: Bool
}
2. Viewが行いたいAction(ロジック)を定義
- Viewから行いたいことはカウントの増減です。
enum Action {
case increase // 値の増加
case decrease // 値の減少
}
3. Actionに対するMutation(変更内容)の定義
Actionから具体的な変更内容を定義します。
※ このサンプルの要件的には、Mutationの定義は省略することができます。詳しくは後述のCounterをリファクタリングを参照してください。 (※ v1.1.0のサンプルではMutationの内容が変更され省略不可になりました)
enum Mutation {
case increaseValue // 値の増加
case decreaseValue // 値の減少
case setLoading(Bool) // ロード中か
}
4. ActionからMutationを生成するロジックを実装
発行されたActionをMutationに変更します。ポイントは複数の Observable.concat()
を使って1つのActionから複数のMutationを生成していることです。Mutation.increaseValueはdelayを使って仮想的に非同期更新しているようなイメージを持っていただくと以下の流れがわかると思います。
- Mutation.setLoading(true)で値の更新前にロード状態をtrueにする
- Mutation.increaseValueで値を更新する
- Mutation.setLoading(false)で値の更新が終わったにロード状態をfalseにする
[補足] Observable.concat()
は複数の Observable を完了するまで待って順次 subscribe します。
// ActionからMutationを生成する
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .increase:
return Observable.concat([
Observable.just(Mutation.setLoading(true)),
Observable.just(Mutation.increaseValue).delay(0.5, scheduler: MainScheduler.instance),
Observable.just(Mutation.setLoading(false)),
])
case .decrease:
return Observable.concat([
Observable.just(Mutation.setLoading(true)),
Observable.just(Mutation.decreaseValue).delay(0.5, scheduler: MainScheduler.instance),
Observable.just(Mutation.setLoading(false)),
])
}
}
5. MutationからStateの内容を更新するロジックを実装
発行されたMutationを元にStateのvalueを増減させます。
// MutationからStateを更新する
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case .increaseValue:
state.value += 1
case .decreaseValue:
state.value -= 1
case let .setLoading(isLoading):
state.isLoading = isLoading
}
return state
}
6. Stateの初期値を定義
中央に表示するカウントの初期値を定義します。
let initialState: State
init() {
self.initialState = State(
value: 0, // カウントする値の初期値は0
isLoading: false
)
}
7. ViewでActionの発行を実装
increaseButton/decreaseButtonのタップイベントをそれぞれのActionに変換してreactor.actionにバインドします。
increaseButton.rx.tap // タップイベント
.map { Reactor.Action.increase } // タップイベントをAction.increaseに変換
.bind(to: reactor.action) // Actionをreactor.actionにバインド
.disposed(by: disposeBag)
decreaseButton.rx.tap // タップイベント
.map { Reactor.Action.decrease } // タップイベントをAction.decreaseに変換
.bind(to: reactor.action) // Actionをreactor.actionにバインド
.disposed(by: disposeBag)
8. Stateの変更から具体的な各Viewを更新
- reactor.state.valueをvalueLabel.rx.textにバインドして、中央の値を表示を更新する。
- reactor.state.isLoadingをactivityIndicatorView.rx.isAnimatingにバインドして、ロード中がわかるようにUIActivityIndicatorViewを更新する。
reactor.state.map { $0.value } // カウントの値
.distinctUntilChanged() // カウントの値に変化があるか
.map { "\($0)" } // カウントの値を文字列に変換
.bind(to: valueLabel.rx.text) // カウントの値の文字列をUILabelのtextにバインド
.disposed(by: disposeBag)
reactor.state.map { $0.isLoading } // ロード中か
.distinctUntilChanged() // 値に変化があるか
.bind(to: activityIndicatorView.rx.isAnimating) // ロード中かをUIActivityIndicatorViewのアニメーションにバインド
.disposed(by: disposeBag)
Counterをリファクタリング (v1.1.0のサンプルでは不可)
[注意] v1.1.0のサンプルではActionとMutationが同定義ではないため以下のリファクタリングはできなくなりました。以下はv0.4.5の時に有効だった内容です。
Counterの実装を見てMutation
って意味ないんじゃないのかなって思った方もいると思います。
実はその通りで、このサンプルの機能的にはMutationは必要ないです。おそらくこのサンプルの目的が、シンプルな例でReactorKitの実装を紹介するということなので意図して設けられているだけだと思われます。
Mutationは、
associatedtype Mutation = Action
となっているので、Mutationで特に行うべきロジックがない場合はActionのみを定義することが許容されています(Mutationを定義しなければActionと同じ定義になります)。
CounterからMutationを取り除くと、次のようにリファクタリングできます。
final class CounterViewReactor: Reactor {
// Viewが行うアクション
enum Action {
case increase
case decrease
}
// Viewの状態(具体的な値を保持)
struct State {
var value: Int
}
let initialState = State(value: 0) // カウントする値の初期値は0
// Mutation(Actionと同定義)からStateを更新する
func reduce(state: State, mutation: Mutation) -> State {
switch mutation {
case .increase:
return State(value: state.value + 1)
case .decrease:
return State(value: state.value - 1)
}
}
}
まとめ
まずはReactorKitの概要ということでシンプルなCounterを例に取り上げました。Viewに必要な数値のカウントアップ/ダウン機能(ビジネスロジック)と現在のカウント数の保持をViewから切り離し、それらをReactorに移譲することができました。正直この規模だとまだメリットは感じられないと思いますが、アプリがより複雑化したときにReactorKitが活きてきます。
個人的には今一番好きなアーキテクチャがReactorKitです。
- アーキテクチャを構築するためのフレームワークなので、正しいアーキテクチャを構築するための学習コストが抑えられる
- 単方向ストリームがすでに構築されているので、MVVMでいうとViewとViewModelとの双方向バインディングの複雑さを軽減できる
- 各責務が明瞭
- 他人のコードを読んでも把握が楽
- 最少は1つ以上のViewに対して1つのReactorを設けるだけなので、一部分だけの導入も可能。1つのアプリが単一のアーキテクチャに縛られる必要はないです😉
何よりも設計思想が好きですね。アーキテクチャの学習/導入/運用コストを大きく上回るだけのメリットを感じています。
次回はさらにReactorKitを掘り下げて見たいと思います。
→ ReactorKit(Flux + Reactive Programming)を学ぶ2 基礎編
※ 本文中の画像はReactorKitから引用