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

相关文章
相关标签/搜索