RxFlow使ってみた

# はじめに

つい最近iOSアプリを開発していた際に、RxFlowなるものを知りました。しかし、RxFlowは英語ですら文献があまりなく、日本語に至ってはほぼ皆無に等しいぐらいなので、せっかくだから今回勉強した内容を書き記しておこうと思います。

（ただ英語の場合は[公式レポジトリ](https://github.com/RxSwiftCommunity/RxFlow)に記載されてるリンクを読めば十分だからかも？しれません）

# RxFlow is 何？

[RxFlow](https://github.com/RxSwiftCommunity/RxFlow)は[RxSwiftCommunity](https://github.com/RxSwiftCommunity)によって運用されてるプロジェクトです。一言で言うと、ビューコントローラーからナビゲーションの機能を切り離すコーディネーターパターンをRxで提供するライブラリです。アプリケーションの状態からリアクティブに画面の遷移を行うことができるようになります。

# RxFlowの各要素

以降では、公式のサンプルであるRxFlowDemoを基にRxFlowで必要となる各概念を見ていきたいと思います。ただし、説明のため実際のサンプルのソースコードを改変（簡易化）しています。

## Step

`Step`はアプリケーションにおけるナビゲーションの状態です。例えば、ログインが必要な状態、ログイン処理が終了した状態などなど。これを基にナビゲーションを行います。特に制約などなかったと思いますが、状態を示すものなので素直に公式のデモと同じように`enum`で定義するのが良いと思います。

```swift
enum DemoStep: Step {

    // Login
    case loginIsRequired
    case userIsLoggedIn

    // Movie
    case moviesAreRequired
    case movieIsPicked (withId: Int)
    case castIsPicked (withId: Int)
}
```

## Flow

`Flow`では`Step`に対してどのようなナビゲーションを行うかを定義します。また`Flow`はプロトコルとして以下の定義を強制します。

```swift
protocol Flow: ... {

    var root: Presentable { get }
    func navigate(to step: Step) -> FlowContributors
}
```

`root`ではそのフローにおいて親となるビューコントローラーやウィンドウ（`UIViewController`や`UIWindow`はRxFlowにて予め`extension`で`Presentable`を実装している）を指定します。ここで指定したビューコントローラーやウィンドウが次のフローに遷移する際に使われます。そして、その次のフローへの遷移（およびそのフロー内でのステップに対する処理）を定義するのが`navigate`関数です。`navigate`関数には次のステップが引数として渡されるので、これを基にどのような遷移をするかを定義します。以下は簡単な`Flow`の例です。

```swift
class TestFlow: Flow {

    private let rootViewController = UINavigationController()
    var root: Presentable {
        return rootViewController
    }

    func navigate(to step: Step) -> FlowContributors {
        guard let step = step as? DemoStep else { return .none }
        switch step {
        case .moviesAreRequired: return navigateToMovieListScreen()
        case .movieIsPicked(let movieId): return navigateToMovieDetailScreen(with: movieId)
        case .castIsPicked(let castId): return navigateToCastDetailScreen(with: castId)
        default: return .none
        }
    }
    // ...
}
```

## Stepper

`Stepper`は次のステップを発行するトリガーです。画面遷移の多くは何かしらの操作を起点とするので、MVVMの場合はビューモデルを`Stepper`として定義することが多くなると思います。また、ステッパーでは基本的に`var steps: PublishRelay<Step> { get }`に対して`accept`することで次のステップを発行しますが、初期値やコールバックなども用意されています。

```swift
protocol Stepper {

    var steps: PublishRelay<Step> { get }
    var initialStep: Step { get }

    func readyToEmitSteps ()
}

extension Stepper {

    var initialStep: Step {
        return NoneStep()
    }

    func readyToEmitSteps () {}
}
```

以下は驚くほどシンプルなステッパーの例です。

```swift
class TestViewModel: Stepper {

    let steps = PublishRelay<Step>()

    func pick(movieId: Int) {
        steps.accept(DemoStep.movieIsPicked(withId: movieId))
    }
}
```

### OneStepper

`Stepper`には初期値だけを持つ`OneStepper`という派生クラスが用意されています。引数として`Step`を与えると、それを`initialStep`として返すだけのクラスです。ビューモデルやユーザーの操作に依存しない`Flow`などに使えます。

```swift
class OneStepper: Stepper {

    let steps = PublishRelay<Step>()
    private let singleStep: Step

    init(withSingleStep singleStep: Step) {
        self.singleStep = singleStep
    }

    public var initialStep: Step {
        return self.singleStep
    }
}
```

### CompositeStepper

ユーザーの操作だけでなく何かイベントに応じてナビゲーションしたいなど、複数のステッパーからステップを変更したい場合は`CompositeStepper`を用いることで可能です。`CompositeStepper`では複数のステッパーの`steps`を`Observable.merge`で1つにまとめて`self.steps`にバインドしています。

```swift
class CompositeStepper: Stepper {

    private let disposeBag = DisposeBag()
    private let innerSteppers: [Stepper]
    let steps = PublishRelay<Step>()

    init(steppers: [Stepper]) {
        self.innerSteppers = steppers
    }

    func readyToEmitSteps() {
        let initialSteps = Observable<Step>.from(self.innerSteppers.map { $0.initialStep })
        let nextSteps = Observable<Step>
            .merge(self.innerSteppers.map { $0.steps.asObservable() })

        initialSteps
            .concat(nextSteps)
            .bind(to: self.steps)
            .disposed(by: self.disposeBag)
    }
}
```

## FlowCoordinator

`FlowCoordinator`は開発者が定義したフローやステップの組み合わせを適切にハンドリングしてくれる・・・らしいです。というのも、このコーディネーターに関してはRxFlowが提供してくれたものを`AppDelegate`で使うだけで、他では一切使いませんでした。[wikipedia](https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%8D%E3%83%BC%E3%82%BF%E3%83%BC)によればコーディネーターとは「ものごとを調整する役の人」らしいですね。

```swift
class AppDelegate: UIResponder, UIApplicationDelegate {

    let disposeBag = DisposeBag()
    var coordinator = FlowCoordinator()
    // ...

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        self.coordinator.rx.willNavigate.subscribe(onNext: { (flow, step) in
            print("will navigate to flow=\(flow) and step=\(step)")
        }).disposed(by: self.disposeBag)

        self.coordinator.rx.didNavigate.subscribe(onNext: { (flow, step) in
            print("did navigate to flow=\(flow) and step=\(step)")
        }).disposed(by: self.disposeBag)

        // ...

        self.coordinator.coordinate(flow: appFlow, with: appStepper)

        return true
    }
}
```

## FlowContributor / FlowContributors

`FlowContributor`は先程出てきた`FlowCoordinator`に対して、次のフローのコントリビューター（新しいステップを発行するもの）を伝えるためのデータ構造体です。`Presentable`と`Stepper`を組み合わせた`enum`ぐらいの認識で良さそうです。一方、`FlowContributors`は次の`FlowContributor`が複数あるのか、一つなのか、ない（何もしない）のか、などを`FlowCoordinator`に対して伝えるための`enum`、`FlowContributor`のコンテナといったところではないでしょうか。この`FlowContributors`が`Flow`の`navigate`関数で出てきたのにしれっと無視してた返り値の型になります。

以下は次のビューコントローラーを表示する際の一例です。

```swift
private func navigateToMovieDetailScreen (with movieId: Int) -> FlowContributors {
    let viewModel = MovieDetailViewModel(withMovieId: movieId)
    let viewController = MovieDetailViewController.instantiate(withViewModel: viewModel)
    self.rootViewController.pushViewController(viewController, animated: true)
    return .one(flowContributor: .contribute(withNextPresentable: viewController, withNextStepper: viewModel))
}
```

この`FlowContributor`および`FlowContributors`の値によってどのようにフローを制御していくかが決まってきます。

# 基本的なナビゲーションのパターン

実際に使ってみないとよく分からないことが多いと思うので、ここでは実例（もちろんRxFlowDemo）を用いて見ていきたいと思います。なお、ここでは`navigate`関数内の`switch`でどのような`FlowContributors`を返すかにフォーカスしていきます。

```swift
func navigate(to step: Step) -> FlowContributors {
    guard let step = step as? DemoStep else { return .none }
    switch step {
    case: .someStep: return navigateSomeScreen()
    default: return .none
    }
}
```

## 方針

ナビゲーションのパターンの前に、前提として`Step`や`Flow`をどのように設計・構成したかという話を簡単にしたいと思います。RxFlow導入当初は「ステップがフロー毎に存在しても良いのではないか？」と思っていたのですが、実際に実装していくにつれRxFlowDemoのようにステップは1つのみにするのが最も良いように思えました。最初は特に問題なさそうだったのですが、2つの異なる画面からある同じ画面に遷移する場合に困りました。仮に`FlowA`と`StepA`のペアと`FlowB`と`StepB`のペアがあり、どちらも同じ画面に遷移したい場合、その画面の`Stepper`が`StepA`か`StepB`のどちらのステップを`accept`したら良いか分からないからです。もちろん、プロトコルなどで制約を設けることで共通化は可能かもしれませんが、どうしても無理矢理感が拭えず、ステップは1つにすべきという結論に至りました。

### ボツになったパターン
![multi_step.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277414/0cb4f5bc-ce8d-7523-0c4e-6b9218d37fc1.png)

### 採用したパターン
![one_step.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277414/8ad5fde4-5352-5b95-0e12-f76e516e8c80.png)

こういった理由でRxFlowDemoにおいてもステップは`DemoStep`のみ定義されていたのかもしれませんね。

## 次のフローを指定して遷移

`AppDelegate`から初期フローを構築していく場合、ログイン画面を表示するか、オンボード画面を表示するかなどの判定を行い、ダッシュボード画面のフローへ遷移するといったナビゲーションを行うと思います。そのような場合は、次の`Flow`と`Stepper`をフローコントリビューターとして`.one`を返します。

```swift
private func navigationToDashboardScreen() -> FlowContributors {
    let dashboardFlow = DashboardFlow()
    Flows.whenReady(flow1: dashboardFlow) { [unowned self] root in
        self.rootViewController.pushViewController(root, animated: false)
    }
    let stepper = OneStepper(withSingleStep: DemoStep.dashboardIsRequired)
    return .one(flowContributor: .contribute(withNextPresentable: dashboardFlow,
                                             withNextStepper: stepper))
}
```

`Flows.whenReady`は次のフローへ遷移する準備ができた際にコールされるコールバックです。ここに`present`や`pushViewController`といったこれまでの`UIKit`を用いて行う遷移の実装を行います。

## 次のステッパーを指定して遷移

`UINavigationController`にて`pushViewController`する場合、次のコントローラーとそのビューモデルを用意すると思います。その際、ビューモデルで画面操作をハンドリングすることが多くなると思いますが、そのような場合はフローは変更せず次のステッパーを指定して遷移することになると思います。

```swift
private func navigateToMovieDetailScreen (with movieId: Int) -> FlowContributors {
    let viewModel = MovieDetailViewModel(withMovieId: movieId)
    let viewController = MovieDetailViewController.instantiate(withViewModel: viewModel)
    self.rootViewController.pushViewController(viewController, animated: true)
    return .one(flowContributor: .contribute(withNextPresentable: viewController,
                                             withNextStepper: viewModel))
}
```

フローを変更した場合とは異なり、`withNextPresentable`には`Flow`ではなくビューコントローラーを渡しています。

## 何も変更せず遷移

先ほどの例と同様に、`UINavigationController`で`pushViewController`したいが、特に画面を遷移せずクローズするしかない場合などは`.none`でフローに影響を与えず遷移することも可能です。また、`popViewController`などで画面をクローズしたい場合にも使えます。

```swift
private func navigateToCastDetailScreen (with castId: Int) -> FlowContributors {
    let viewModel = CastDetailViewModel(withCastId: castId)
    let viewController = CastDetailViewController.instantiate(withViewModel: viewModel)
    self.rootViewController.pushViewController(viewController, animated: true)
    return .none
}
```

## 複数のフローを指定して遷移

`UITabBarController`などはタブ毎に独立したナビゲーションを行うため、それぞれのタブ毎にフローを用意する必要があると思います。そのようなときは`.multiple`を用いて複数のフローをコントロールする必要があります。RxFlowではフローとステップの組み合わせで制御するため、一方のフローでステップを変更したとしても、もう一方のフローには影響がありません。それぞれ独立したフローとして定義することができます。

```swift
private func navigateToDashboard() -> FlowContributors {
    let wishListFlow = WishlistFlow()
    let watchedFlow = WatchedFlow()

    Flows.whenReady(flow1: wishListFlow, flow2: watchedFlow) { [unowned self] (root1, root2) in
        let tabBarItem1 = UITabBarItem(title: "Wishlist", image: nil, selectedImage: nil)
        let tabBarItem2 = UITabBarItem(title: "Watched", image: nil, selectedImage: nil)
        root1.tabBarItem = tabBarItem1
        root2.tabBarItem = tabBarItem2
        self.rootViewController.setViewControllers([root1, root2], animated: false)
    }

    return .multiple(flowContributors: [.contribute(withNextPresentable: wishListFlow,
                                                    withNextStepper: OneStepper(withSingleStep: DemoStep.moviesAreRequired)),
                                        .contribute(withNextPresentable: watchedFlow,
                                                    withNextStepper: OneStepper(withSingleStep: DemoStep.moviesAreRequired))])
    }
```

## フローの終了

ビューコントローラーから`present`した`UINavigation`をクローズする場合、現在のフローを終了することになると思います。そのようなときは`.end`を使用します。

```swift
class ParentFlow: Flow {

    // ...

    func navigate(to step: Step) -> FlowContributors {
        guard let step = step as? DemoStep else { return .none }
        switch step {
        case: .settingsAreComplete:
            self.rootViewController.presentedViewController?.dismiss(animated: true)
            return .none
        default: return .none
        }
    }
}

class ChildFlow: Flow {

    // ...

    func navigate(to step: Step) -> FlowContributors {
        guard let step = step as? DemoStep else { return .none }
        switch step {
        case: .settingsAreComplete:
            return .end(forwardToParentFlowWithStep: DemoStep.settingsAreComplete)
        default: return .none
        }
    }
}
```

# 導入してみた感想

### やはり最初は難しい

ナビゲーションの機能をビューコントローラーから分離するという点に関してはVIPERを知っていたのでなんとなく想像できたのですが、コーディネーターパターンは名前しか知りませんでした。また、各要素の概念は名前からなんとなく理解はできるものの、実際に使ってみるまでフロー間の関係などがイマイチ分かりませんでした。

### 慣れると便利

慣れてくるとフローを定義してステッパーでステップを変更するという基本の繰り返しであることが分かりました。ステップに対してフローでリアクティブに遷移をハンドリングするという感じですかね。また、フローの切り分け方も最初は悩みましたが、`root`を基準に考えてフローを作っていくとそこまで難しい話ではないように思えてきました。

### その他

サンプルを参考にしながら実装するだけでDIも簡単にできるため、ビューコントローラーの再利用性が向上しそうなのも良かったです。また、様々なパターンを想定して設計せずとも、少なくともRxFlowDemoで実装されているようなパターンに関しては、レポジトリを参考にするだけで簡単に実装できるのもありがたいです。まだまだ使い始めたばかりですが、分からないことがあった場合はRxFlowDemoのソースコードを読めば大抵なんとかなりそうな気がします。