ReactorKit

Swift ReactorKit 框架

ReactorKit

ReactorKit 是一个响应式、单向 Swift 应用框架。下面来介绍一下 ReactorKit 当中的基本概念和使用方法。

目录

基本概念

ReactorKit 是 FluxReactive Programming 的混合体。用户的操作和视图 view 的状态通过可被观察的流传递到各层。这些流是单向的:视图 view 仅能发出操作(action)流 ,反应堆仅能发出状态(states)流。

flow

设计目标

  • 可测性:ReactorKit 的首要目标是将业务逻辑从视图 view 上分离。这可以让代码方便测试。一个反应堆不依赖于任何 view。这样就只需要测试反应堆和 view 数据的绑定。测试方法可点击查看
  • 侵入小:ReactorKit 不要求整个应用采用这一种框架。对于一些特殊的 view,可以部分的采用 ReactorKit。对于现存的项目,不需要重写任何东西,就可以直接使用 ReactorKit。
  • 更少的键入:对于一些简单的功能,ReactorKit 可以减少代码的复杂度。和其他的框架相比,ReactorKit 需要的代码更少。可以从一个简单的功能开始,逐渐扩大使用的范围。

View

View 用来展示数据。 view controller 和 cell 都可以看做一个 view。�view 需要做两件事:(1)绑定用户输入的操作流,(2)将状态流绑定到 view 对应的 UI 元素。view 层没有业务逻辑,只负责绑定操作流和状态流。

定义一个 view,只需要将一个现存的类符合协议 View。然后这个类就自动有了一个 reactor 的属性。view 的这个属性通常由外界设置。

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 加载结束之后进行绑定的。

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()

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值