ReactorKit/ReactorKit コードレビュー1 (Reactor&View)

概要

github.com

導入

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アーキテクチャがベースであると言えそう

https://cloud.githubusercontent.com/assets/931655/25073432/a91c1688-2321-11e7-8f04-bf91031a09dd.png

実装を見ていく

登場人物

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は必要な定義を書いて組み合わせを書いてくのでシンプルに

次は実装してみてどうモデルレイヤーと繋げるかを試していく

参考

qiita.com