iOSエンジニアリング

[iOSアーキテクチャ #9] ReactorKitまとめ、単方向アーキテクチャの標準

ReactorKitはUIKitとRxSwiftの組み合わせで、単方向データフローを標準のようにしたフレームワークです。ViewとReactorの2つの役割、Mutationという中間段階が必要な理由、TCAとの違いを整理します。

読了 6 分
[iOSアーキテクチャ #9] ReactorKitまとめ、単方向アーキテクチャの標準のカバー画像

TCA(The Composable Architecture)の記事では単方向データフローを扱いました。実は韓国のiOS界隈には、それより前に単方向を標準のように定着させたフレームワークがあります。それがReactorKitです。

全秀烈(Suyeol Jeon)氏が2017年に公開したこのフレームワークは、StyleShareやカカオ系列のサービスが導入したことで、UIKit + RxSwiftの事実上の標準アーキテクチャになりました。求人広告に「ReactorKit経験者優遇」が頻繁に登場した時期さえあります。

今回はReactorKitの構造、TCAとの違い、そして現在の位置付けを整理します。


ViewとReactor、役割は2つだけ

ReactorKitの構造はシンプルです。1つの画面をViewとReactorに分けます。

  • View:ユーザー入力をActionへ流し、Stateを購読して画面を描画します。ロジックは持ちません。
  • Reactor:Actionを受け取り、Stateを生成するロジックの塊です。UIのことは一切知りません。

重要なのは、Reactor内部のデータフローが一方向に固定されている点です。

Action → mutate() → Mutation → reduce() → State → View

ボタンタップがAction.refreshとして入ると、mutate()がネットワーク要求などの副作用を処理し、Mutation.setItems([...])を発行します。reduce()はこのMutationを受け取り、新しいStateを計算します。ViewはStateを購読し、画面を更新します。

Mutationという中間段階はなぜ必要なのか

MVVM(Model-View-ViewModel)に慣れていると、「Actionから直接Stateを変えればよいのでは?」と疑問に思うかもしれません。Mutationを挟むのは、非同期処理を隔離するためです。

mutate()は非同期処理が許される唯一の場所です。一方、reduce()は純粋関数なので、同じStateとMutationを渡せば常に同じ結果になります。副作用が1か所に固定されるため、状態がなぜ変わったのか追跡するときに見る場所も1か所です。

final class SearchReactor: Reactor {
    enum Action { case updateQuery(String) }
    enum Mutation { case setResults([Repo]) }

    struct State { var results: [Repo] = [] }

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .updateQuery(let query):
            return service.search(query)
                .map { Mutation.setResults($0) }
        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .setResults(let repos): newState.results = repos
        }
        return newState
    }
}

ReactorはUIを知らない純粋なロジックなので、テストも簡単です。Actionを与えてStateを確認すれば終わりです。

ReactorKitのAction mutate Mutation reduce Stateによる単方向フロー図
非同期処理はmutate()に、純粋な計算はreduce()に隔離されます

TCAとは何が違うのか

どちらもReduxの影響を受けた単方向アーキテクチャですが、適用単位が異なります。

ReactorKit TCA
基盤 RxSwift Combine·Swift Concurrency
適用単位 画面(View)ごとにReactorを1つ 機能単位のReducerをツリー状に合成
導入方法 画面ごとに段階的に導入可能 アプリ全体を1つの体系として構成
主な対象 UIKit SwiftUI

ReactorKitの実用性は導入方法にあります。既存のMVC(Model-View-Controller)プロジェクトでも、1つの画面を選んでReactorを追加すれば動作します。アプリ全体を書き換える必要はありません。一方TCAは、機能間の状態合成までフレームワークが管理する代わりに、体系全体を受け入れる必要があります。

ちなみにReduxをiOSへ直接移植したReSwiftというライブラリもありましたが、アプリ全体で単一ストアを使う方式はUIKitとの相性が悪く、ReactorKitほど広まりませんでした。

現在のReactorKit

正直なところ、新規プロジェクトでReactorKitを選ぶケースは減っています。弱点が明確になったためです。

1つ目は、RxSwiftに強く結合していることです。AppleのエコシステムがCombineとSwift Concurrencyへ移行する中、RxSwift依存そのものが負担になりました。

2つ目は、SwiftUIと相性がよくないことです。Viewプロトコルとバインディング方式が、UIKitの命令型更新を前提に設計されています。

それでもReactorKitが残した功績は明らかです。「状態は一方向にだけ流れる」「副作用は1か所に隔離する」という感覚を、韓国のiOS開発者にいち早く根付かせたのがこのフレームワークです。今TCAを学んでいる人なら、Action・Mutation・Stateの構造に見覚えがあるでしょう。その感覚の多くはReactorKit時代に形作られました。

既存のUIKit + RxSwiftコードベースを保守しているなら、ReactorKitは今も実績のある選択肢です。画面単位で追加・削除できる構造なので、後から画面ごとに取り除いて移行するのも比較的容易です。

ReactorKitからTCAへ単方向フローのバトンを渡すリレーイラスト
単方向という感覚を先に根付かせたのがReactorKitで、TCAがそれを受け継ぎました

まとめ

  • ReactorKitは、1つの画面をViewとReactorに分け、Action → Mutation → Stateという単方向フローを強制するRxSwiftベースのアーキテクチャです。
  • 非同期処理はmutate()に、純粋な状態計算はreduce()に隔離されます。そのため追跡とテストが容易です。
  • TCAと概念は同じですが、画面単位で段階的に導入できる実用性が差別化ポイントです。
  • RxSwiftとの結合やSwiftUI非対応のため新規導入は減りましたが、UIKitのレガシーコードでは今も有効です。

次回は、画面ではなくビジネスロジック単位でアプリを分割するUberのRIBsを扱います。VIPERの記事で一段落だけ触れた、あのアーキテクチャを詳しく掘り下げます。


参考資料

あわせて読みたい