ReactorKit/ReactorKit コードレビュー1 (Reactor&View)
概要
導入
cocoapods
pod 'ReactorKit' pod 'RxSwift', '~> 4.0.0' pod 'RxCocoa', '~> 4.0.0'
carthage
github 'ReactorKit' github 'RxSwift', '~> 4.0.0' github 'RxCocoa', '~> 4.0.0'
exampleを見てみる
exampleがあったのでそこから見ていく、GitHub APIを叩いてリポジトリを検索してるであろうコード
ReactorKit/GitHubSearchViewController.swift at master · ReactorKit/ReactorKit · GitHub
そしてここからフレームワークとして必須な部分だけを抜き出していく
ViewController
class GitHubSearchViewController: UIViewController, StoryboardView { var disposeBag = DisposeBag() func bind(reactor: GitHubSearchViewReactor) { // Action searchController.searchBar.rx.text .throttle(0.3, scheduler: MainScheduler.instance) .map { Reactor.Action.updateQuery($0) } .bind(to: reactor.action) .disposed(by: disposeBag) } }
StoryboardViewプロトコルに準拠してbind関数さえ実装していれば良さそう
次にここで出てくるGitHubSearchViewReactorを見ていく
Reactor
final class GitHubSearchViewReactor: Reactor { enum Action { case updateQuery(String?) case loadNextPage } enum Mutation { case setQuery(String?) case setRepos([String], nextPage: Int?) case appendRepos([String], nextPage: Int?) } struct State { var query: String? var repos: [String] = [] var nextPage: Int? } let initialState = State() func mutate(action: Action) -> Observable<Mutation> { switch action { case let .updateQuery(query): return Observable.concat([ // 1) set current state's query (.setQuery) Observable.just(Mutation.setQuery(query)), // 2) call API and set repos (.setRepos) self.search(query: query, page: 1) // cancel previous request when the new `.updateQuery` action is fired .takeUntil(self.action.filter(isUpdateQueryAction)) .map { Mutation.setRepos($0, nextPage: $1) }, ]) } } func reduce(state: State, mutation: Mutation) -> State { switch mutation { case let .setQuery(query): var newState = state newState.query = query return newState case let .setRepos(repos, nextPage): var newState = state newState.repos = repos newState.nextPage = nextPage return newState case let .appendRepos(repos, nextPage): var newState = state newState.repos.append(contentsOf: repos) newState.nextPage = nextPage return newState } }
mutate(action: Action) -> Observable<Mutation>
から
reduce(state: State, mutation: Mutation) -> State
に繋がる流れになっている、これだけ見ればfluxアーキテクチャがベースであると言えそう
実装を見ていく
登場人物
Reactorプロトコル
定義
public protocol Reactor: class, AssociatedObjectStore { associatedtype Action associatedtype Mutation = Action associatedtype State var action: ActionSubject<Action> { get } var initialState: State { get } var currentState: State { get } var state: Observable<State> { get } func transform(action: Observable<Action>) -> Observable<Action> func mutate(action: Action) -> Observable<Mutation> func transform(mutation: Observable<Mutation>) -> Observable<Mutation> func reduce(state: State, mutation: Mutation) -> State func transform(state: Observable<State>) -> Observable<State> }
流れ
まず上の例の .bind(to: reactor.action)
でreactorのactionのgetterに触る
そうするとactionが_actionと_stateに伝播する
public var action: ActionSubject<Action> { // Creates a state stream automatically _ = self._state // It seems that Swift has a bug in associated object when subclassing a generic class. This is // a temporary solution to bypass the bug. See #30 for details. return self._action }
そして発火した_stateからcreateStateStream()
が発火する、
private var _state: Observable<State> { if self.stub.isEnabled { return self.stub.state.asObservable() } else { return self.associatedObject(forKey: &stateKey, default: self.createStateStream()) } }
ここが根底の処理となっていて
Reactorで定義したstreamたちを練り合わせている
public func createStateStream() -> Observable<State> { let action = self._action.asObservable() let transformedAction = self.transform(action: action) let mutation = transformedAction .flatMap { [weak self] action -> Observable<Mutation> in guard let `self` = self else { return .empty() } return self.mutate(action: action).catchError { _ in .empty() } } let transformedMutation = self.transform(mutation: mutation) let state = transformedMutation .scan(self.initialState) { [weak self] state, mutation -> State in guard let `self` = self else { return state } return self.reduce(state: state, mutation: mutation) } .catchError { _ in .empty() } .startWith(self.initialState) .observeOn(MainScheduler.instance) let transformedState = self.transform(state: state) .do(onNext: { [weak self] state in self?.currentState = state }) .replay(1) transformedState.connect().disposed(by: self.disposeBag) return transformedState }
この流れを省略するとこうなる
Action -> transform(Action->Action) -> mutate(Action->Mutation) -> reduce(State,Mutation -> State) -> transform(State->State)
Viewプロトコル
UIViewController、UIViewに準拠させる際のベースプロトコル
public protocol View: class, AssociatedObjectStore { associatedtype Reactor: _Reactor var disposeBag: DisposeBag { get set } var reactor: Reactor? { get set } func bind(reactor: Reactor) }
bindで指定した型をpropertyのreactorとして返すように裏で動く模様、
let viewController = navigationController.viewControllers.first as! GitHubSearchViewController viewController.reactor = GitHubSearchViewReactor()
(訂正): 画面遷移前に渡してた
/// Creates RxSwift bindings. This method is called each time the `reactor` is assigned. /// /// Here is a typical implementation example: /// /// ``` /// func bind(reactor: MyReactor) { /// // Action /// increaseButton.rx.tap /// .bind(to: Reactor.Action.increase) /// .disposed(by: disposeBag) /// /// // State /// reactor.state.map { $0.count } /// .bind(to: countLabel.rx.text) /// .disposed(by: disposeBag) /// } /// ``` /// /// - warning: It's not recommended to call this method directly.
reactorのプロパティを生やすの、どうやってるんだろうと思ったら案の定ランタイムをいじってた
あと直接呼ぶなってあるのでライフサイクル関数の扱いっぽい
extension View { public var reactor: Reactor? { get { return self.associatedObject(forKey: &reactorKey) } set { self.setAssociatedObject(newValue, forKey: &reactorKey) self.disposeBag = DisposeBag() if let reactor = newValue { self.bind(reactor: reactor) } } } }
まとめ
ここまで見るとRxを使う上で設計の指針が何もない場合はベースとして導入するのはありな感じ
ViewControllerはbindを、reactorは必要な定義を書いて組み合わせを書いてくのでシンプルに
次は実装してみてどうモデルレイヤーと繋げるかを試していく