はじめての ReactorKit【実践編】
前回の概要編に続き、今回は実際に ReactorKit を使ったサンプル実装をしていきたいと思います。
作るアプリ
今回は Google Books API を使用したアプリを想定して実装していきたいと思います。仕様は下記の通りです。 ※ 基本的には、ReactorKit 周りの実装が中心なので細かい API 処理などの実装部分などは省いていきます。
- 画面が開いたら、API からデータを取得して TableView に反映する
refreshButton
がタップされたら、データを更新する- API からデータを取得している最中は
activityIndicator
を表示し、取得が完了したら非表示にする
作業開始👨💻
まずは View のロジックを担う Reactor にそれぞれのイベントとデータを定義していきます。
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:)
関数を定義していきます。
// 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
のように1つのイベントで 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:)
が呼ばれるようになります。
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)。
こんな感じで、ReactorKit を使ってシンプルにアプリを作成することができました🎉 また他の場面で使用することがあったらまた記事を書きたいと思います。
参考
- https://github.com/ReactorKit/ReactorKit
- https://qiita.com/yusuga/items/e793963ff51ee493497a
- https://www.wantedly.com/companies/wantedly/post_articles/127635