はじめての ReactorKit【実践編】

前回の概要編に続き、今回は実際に ReactorKit を使ったサンプル実装をしていきたいと思います。



### 作るアプリ

今回は [Google Books API](https://developers.google.com/books) を使用したアプリを想定して実装していきたいと思います。仕様は下記の通りです。
※ 基本的には、ReactorKit 周りの実装が中心なので細かい API 処理などの実装部分などは省いていきます。

- 画面が開いたら、API からデータを取得して TableView に反映する
- `refreshButton` がタップされたら、データを更新する
- API からデータを取得している最中は `activityIndicator` を表示し、取得が完了したら非表示にする


### 作業開始👨‍💻

まずは View のロジックを担う Reactor にそれぞれのイベントとデータを定義していきます。

```swift
import ReactorKit
import RxSwift
import RxCocoa // 後々 concat() 関数も使いたいのでインポート

class BookListViewReactor: Reactor {
    enum Action {
        case load
        case refresh
    }

    // Mutation を定義しない場合は、Action が Mutation として扱われる
    enum Mutation {
        case setBooks([ServerBook])
        case setLoading(Bool)
    }

    struct State {
        var books: [ServerBook]
        var isLoading: Bool
    }

    var initialState: BookListViewReactor.State = State(books: [], isLoading: false)
}
```

`Action`・`State`・`initialState` あたりは必須 Reactor プロトコルの定義で必須で、`Mutation` に関しては、Action をもとに実行される処理の具体的な結果の値を定義します。また、`Mutation` の定義は必須ではなく、定義が無い場合には `Action` が `Mutation` として扱われます。


次に `Action` をもとに処理を実行して `Mutation` を返すための `mutate(action:)` と、その受け取った `Mutation` をもとに新しい `State` を返す `reduce(state:, mutation:)` 関数を定義していきます。

```swift
    // BookListViewReactor

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .load:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                // API から本の情報一覧を取得
                BookService.sharedInstance.getAllBooks().flatMap { item in Observable.just(Mutation.setBooks(item.items))},
                Observable.just(Mutation.setLoading(false))
            ])
        case .refresh:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                BookService.sharedInstance.getAllBooks().flatMap { item in Observable.just(Mutation.setBooks(item.items))},
                Observable.just(Mutation.setLoading(false))
            ])
        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        switch mutation {
        case .setLoading(let isLoading):
            var newState = state
            newState.isLoading = isLoading
            return newState
        case .setBooks(let books):
            var newState = state
            newState.books = books
            return newState
        }
    }
```

`mutate(action:)` の中では、主に `Observable.cancat()` で `Mutaiont` を返しています。今回 Action として定義した `load` のように１つのイベントで `loading の更新` や `API からデータを取得` など複数の処理を行う必要があるので、RxCocoa が提供している `concat()` 関数で Observable を直列に実行して、順次 `Mutatation` を返しています。また、`reduce(state:, mutation:)` では受け取った `Mutation` と現在の `State` をもとに新しい `State` を発行して返しています。

これで、Reactor 側の実装は完了したので、View の実装をしていきます。

まずは、使用する View(ViewController) を `ReactorKit` が提供している `View` プロトコルに準拠させます。また、今回は、Storyboard を使用して View を作成していくので、`StoryboardView` というプロトコルに準拠させます。これによって、ViewController の childViews が初期化されたタイミングで `bind(reactor:)` が呼ばれるようになります。

```swift
import ReactorKit
import RxSwift

class BookListViewController: UIViewController, StoryboardView {
    @IBOutlet weak var refreshButton: UIButton!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    @IBOutlet weak var tableView: UITableView!
    // 🚨 本来はより、Testable にするために Reactor の注入はクラス内では行いませんが、今回はサンプル実装のためこのままでいきます
    var reactor: BookListViewReactor? = BookListViewReactor()
    
    var disposeBag: DisposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UINib(nibName: "BookTableViewCell", bundle: nil), forCellReuseIdentifier: "BookTableViewCell")
    }

    func bind(reactor: BookListViewReactor) {
        Observable.just(Void())
            .map { Reactor.Action.load }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

        refreshButton.rx.tap
            .map { Reactor.Action.refresh }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

        // State binding.
        reactor.state
            .map { $0.isLoading }
            .distinctUntilChanged() // 値に変更があった場合にのみイベントを流す
            .map { !$0 }
            .bind(to: activityIndicator.rx.isHidden)
            .disposed(by: disposeBag)
        
        reactor.state
            .map { $0.books }
            .bind(to: tableView.rx.items(cellIdentifier: "BookTableViewCell", cellType: BookTableViewCell.self)) { index, book, cell in
                cell.set(book: book)
            }
            .disposed(by: disposeBag)
    }

}
```

基本的には、`disposeBag` と `bind(reactor:)` の定義が必須になります。今回はサンプル実装なので、`reactor` への反映をクラス内で行っていますが、本来はより Testable にするために、切り離す必要があります(初回表示の View の場合は、AppDelegate 内で reactor の反映を行うなど)。また、`bind(reactor:)` が呼ばれるのは、`reactor` への反映が完了していて、かつ `viewDidLoad` の後に呼び出されます。ですので、初回時に行う処理などを `bind(reactor:)` で `rx.methodInvoked(#selector(viewDidLoad))` のように Observe したいところですが、これは呼ばれないので初回時の処理は `Observable.just(Void())` で定義します(参考の[Issue-comment](https://github.com/devxoul/RxViewController/issues/7#issuecomment-357522455))。


こんな感じで、ReactorKit を使ってシンプルにアプリを作成することができました🎉 また他の場面で使用することがあったらまた記事を書きたいと思います。

### 参考

- https://github.com/ReactorKit/ReactorKit
- https://qiita.com/yusuga/items/e793963ff51ee493497a
- https://www.wantedly.com/companies/wantedly/post_articles/127635