Swift ReactorKit 框架

ReactorKit 是一個響應式、單向 Swift 應用框架。下面來介紹一下 ReactorKit 當中的基本概念和使用方法。react

目錄

基本概念

ReactorKit 是 FluxReactive Programming 的混合體。用戶的操做和視圖 view 的狀態經過可被觀察的流傳遞到各層。這些流是單向的:視圖 view 僅能發出操做(action)流 ,反應堆僅能發出狀態(states)流。git

flow

設計目標

  • 可測性:ReactorKit 的首要目標是將業務邏輯從視圖 view 上分離。這可讓代碼方便測試。一個反應堆不依賴於任何 view。這樣就只須要測試反應堆和 view 數據的綁定。測試方法可點擊查看
  • 侵入小:ReactorKit 不要求整個應用採用這一種框架。對於一些特殊的 view,能夠部分的採用 ReactorKit。對於現存的項目,不須要重寫任何東西,就能夠直接使用 ReactorKit。
  • 更少的鍵入:對於一些簡單的功能,ReactorKit 能夠減小代碼的複雜度。和其餘的框架相比,ReactorKit 須要的代碼更少。能夠從一個簡單的功能開始,逐漸擴大使用的範圍。

View

View 用來展現數據。 view controller 和 cell 均可以看作一個 view。view 須要作兩件事:(1)綁定用戶輸入的操做流,(2)將狀態流綁定到 view 對應的 UI 元素。view 層沒有業務邏輯,只負責綁定操做流和狀態流。github

定義一個 view,只須要將一個現存的類符合協議 View。而後這個類就自動有了一個 reactor 的屬性。view 的這個屬性一般由外界設置。swift

class ProfileViewController: UIViewController, View {
  var disposeBag = DisposeBag()
}

profileViewController.reactor = UserViewReactor() // inject reactor
複製代碼

當這個 reactor 屬性被設置(或修改)的時候,將自動調用 bind(reactor:) 方法。view 經過實現 bind(reactor:) 來綁定操做流和狀態流。閉包

func bind(reactor: ProfileViewReactor) {
  // action (View -> Reactor)
  refreshButton.rx.tap.map { Reactor.Action.refresh }
    .bind(to: reactor.action)
    .disposed(by: self.disposeBag)

  // state (Reactor -> View)
  reactor.state.map { $0.isFollowing }
    .bind(to: followButton.rx.isSelected)
    .disposed(by: self.disposeBag)
}
複製代碼

Storyboard 的支持

若是使用 storyboard 來初始一個 view controller,則須要使用 StoryboardView 協議。StoryboardView 協議和 View 協議相比,惟一不一樣的是 StoryboardView 協議是在 view 加載結束以後進行綁定的。app

let viewController = MyViewController()
viewController.reactor = MyViewReactor() // will not executes `bind(reactor:)` immediately

class MyViewController: UIViewController, StoryboardView {
  func bind(reactor: MyViewReactor) {
    // this is called after the view is loaded (viewDidLoad)
  }
}
複製代碼

Reactor 反應堆

反應堆 Reactor 層,和 UI 無關,它控制着一個 view 的狀態。reactor 最主要的做用就是將操做流從 view 中分離。每一個 view 都有它對應的反應堆 reactor,而且將它全部的邏輯委託給它的反應堆 reactor。框架

定義一個 reactor 時須要符合 Reactor 協議。這個協議要求定義三個類型: Action, MutationState,另外它須要定義一個名爲 initialState 的屬性。異步

class ProfileViewReactor: Reactor {
  // represent user actions
  enum Action {
    case refreshFollowingStatus(Int)
    case follow(Int)
  }

  // represent state changes
  enum Mutation {
    case setFollowing(Bool)
  }

  // represents the current view state
  struct State {
    var isFollowing: Bool = false
  }

  let initialState: State = State()
}
複製代碼

Action 表示用戶操做,State 表示 view 的狀態,MutationActionState 之間的轉化橋樑。reactor 將一個 action 流轉化到 state 流,須要兩步:mutate()reduce()ide

flow-reactor

mutate()

mutate() 接受一個 Action,而後產生一個 Observable<Mutation>測試

func mutate(action: Action) -> Observable<Mutation>
複製代碼

全部的反作用應該在這個方法內執行,好比異步操做,或者 API 的調用。

func mutate(action: Action) -> Observable<Mutation> {
  switch action {
  case let .refreshFollowingStatus(userID): // receive an action
    return UserAPI.isFollowing(userID) // create an API stream
      .map { (isFollowing: Bool) -> Mutation in
        return Mutation.setFollowing(isFollowing) // convert to Mutation stream
      }

  case let .follow(userID):
    return UserAPI.follow()
      .map { _ -> Mutation in
        return Mutation.setFollowing(true)
      }
  }
}
複製代碼

reduce()

reduce() 由當前的 State 和一個 Mutation 生成一個新的 State

func reduce(state: State, mutation: Mutation) -> State
複製代碼

這個應該是一個簡單的方法。它應該僅僅同步的返回一個新的 State。不要在這個方法內執行任何有反作用的操做。

func reduce(state: State, mutation: Mutation) -> State {
  var state = state // create a copy of the old state
  switch mutation {
  case let .setFollowing(isFollowing):
    state.isFollowing = isFollowing // manipulate the state, creating a new state
    return state // return the new state
  }
}
複製代碼

transform()

transform() 用來轉化每一種流。這裏包含三種 transforms() 的方法。

func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>
複製代碼

經過這些方法能夠將流進行轉化,或者將流和其餘流進行合併。例如:在合併全局事件流時,最好使用 transform(mutation:) 方法。點擊查看全局狀態的更多信息。

另外,也能夠經過這些方法進行測試。

func transform(action: Observable<Action>) -> Observable<Action> {
  return action.debug("action") // Use RxSwift's debug() operator
}
複製代碼

高級用法

Global States (全局狀態)

和 Redux 不一樣, ReactorKit 不須要一個全局的 app state,這意味着你能夠使用任何類型來管理全局 state,例如用 BehaviorSubject,或者 PublishSubject,甚至一個 reactor。ReactorKit 不須要一個全局狀態,因此無論應用程序有多特殊,均可以使用 ReactorKit。

Action → Mutation → State 流中,沒有使用任何全局的狀態。你能夠使用 transform(mutation:) 將一個全局的 state 轉化爲 mutation。例如:咱們使用一個全局的 BehaviorSubject 來存儲當前受權的用戶,當 currentUser 變化時,須要發出 Mutation.setUser(User?),則能夠採用下面的方案:

var currentUser: BehaviorSubject<User> // global state

func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
    return Observable.merge(mutation, currentUser.map(Mutation.setUser))
}
複製代碼

這樣,當 view 每次向 reactor 產生一個 action 或者 currentUser 改變的時候,都會發送一個 mutation。

View Communication (View 通訊)

多個 view 之間通訊時,一般會採用回調閉包或者代理模式。ReactorKit 建議採用 reactive extensions 來解決。最多見的 ControlEvent 示例是 UIButton.rx.tap。關鍵思路就是將自定義的視圖轉化爲像 UIButton 或者 UILabel 同樣。

view-view

假設咱們有一個 ChatViewController 來展現消息。 ChatViewController 有一個 MessageInputView,當用戶點擊 MessageInputView 上的發送按鈕時,文字將會發送到 ChatViewController,而後 ChatViewController 綁定到對應的 reactor 的 action。下面是 MessageInputView 的 reactive extensions 的一個示例:

extension Reactive where Base: MessageInputView {
    var sendButtonTap: ControlEvent<String> {
        let source = base.sendButton.rx.tap.withLatestFrom(...)
        return ControlEvent(events: source)
    }
}
複製代碼

這樣就是能夠在 ChatViewController 中使用這個擴展。例如:

messageInputView.rx.sendButtonTap
  .map(Reactor.Action.send)
  .bind(to: reactor.action)
複製代碼

Testing 測試

ReactorKit 有一個用於測試的 built-in 功能。經過下面的指導,你能夠很容易測試 view 和 reactor。

測試內容

首先,你要肯定測試內容。有兩個方面須要測試,一個是 view 或者一個是 reactor。

  • View
    • Action: 可否經過給定的用戶交互發送給 reactor 對應的 action?
    • State: view 可否根據給定的 state 對屬性進行正確的設置?
  • Reactor
    • State: state 可否根據 action 進行相應的修改?

View 測試

view 能夠根據 stub reactor 進行測試。reactor 有一個 stub 的屬性,它能夠打印 actions,而且強制修改 states。若是啓用了 reactor 的 stub,mutate()reduce() 將不會被執行。stub 有下面幾個屬性:

var isEnabled: Bool { get set }
var state: StateRelay<Reactor.State> { get }
var action: ActionSubject<Reactor.Action> { get }
var actions: [Reactor.Action] { get } // recorded actions
複製代碼

下面是一些測試示例:

func testAction_refresh() {
  // 1. prepare a stub reactor
  let reactor = MyReactor()
  reactor.stub.isEnabled = true

  // 2. prepare a view with a stub reactor
  let view = MyView()
  view.reactor = reactor

  // 3. send an user interaction programatically
  view.refreshControl.sendActions(for: .valueChanged)

  // 4. assert actions
  XCTAssertEqual(reactor.stub.actions.last, .refresh)
}

func testState_isLoading() {
  // 1. prepare a stub reactor
  let reactor = MyReactor()
  reactor.stub.isEnabled = true

  // 2. prepare a view with a stub reactor
  let view = MyView()
  view.reactor = reactor

  // 3. set a stub state
  reactor.stub.state.value = MyReactor.State(isLoading: true)

  // 4. assert view properties
  XCTAssertEqual(view.activityIndicator.isAnimating, true)
}
複製代碼

測試 Reactor

reactor 能夠被單獨測試。

func testIsBookmarked() {
    let reactor = MyReactor()
    reactor.action.onNext(.toggleBookmarked)
    XCTAssertEqual(reactor.currentState.isBookmarked, true)
    reactor.action.onNext(.toggleBookmarked)
    XCTAssertEqual(reactor.currentState.isBookmarked, false)
}
複製代碼

一個 action 有時會致使 state 屢次改變。好比,一個 .refresh action 首先將 state.isLoading 設置爲 true,並在刷新結束後設置爲 false。在這種狀況下,很難用 currentState 測試 stateisLoading 的狀態更改過程。這時,你能夠使用 RxTestRxExpect。下面是使用 RxExpect 的測試案例:

func testIsLoading() {
  RxExpect("it should change isLoading") { test in
    let reactor = test.retain(MyReactor())
    test.input(reactor.action, [
      next(100, .refresh) // send .refresh at 100 scheduler time
    ])
    test.assert(reactor.state.map { $0.isLoading })
      .since(100) // values since 100 scheduler time
      .assert([
        true,  // just after .refresh
        false, // after refreshing
      ])
  }
}
複製代碼

Scheduling 調度

定義 scheduler 屬性來指定發出和觀察的狀態流的 scheduler。注意:這個隊列 必須 是一個串行隊列。scheduler 的默認值是 CurrentThreadScheduler

final class MyReactor: Reactor {
  let scheduler: Scheduler = SerialDispatchQueueScheduler(qos: .default)

  func reduce(state: State, mutation: Mutation) -> State {
    // executed in a background thread
    heavyAndImportantCalculation()
    return state
  }
}
複製代碼

示例

  • Counter: The most simple and basic example of ReactorKit
  • GitHub Search: A simple application which provides a GitHub repository search
  • RxTodo: iOS Todo Application using ReactorKit
  • Cleverbot: iOS Messaging Application using Cleverbot and ReactorKit
  • Drrrible: Dribbble for iOS using ReactorKit (App Store)
  • Passcode: Passcode for iOS RxSwift, ReactorKit and IGListKit example
  • Flickr Search: A simple application which provides a Flickr Photo search with RxSwift and ReactorKit
  • ReactorKitExample

依賴

其餘

其餘信息能夠查看 github

相關文章
相關標籤/搜索