ReactorKit 3.2.0

ReactorKit 3.2.0

测试测试
Lang语言 SwiftSwift
许可证 MIT
发布最后发布2022年1月
SPM支持 SPM

Suyeol JeonKanghoon Oh 维护。



 
依赖
RxSwift~> 6.0
WeakMapTable~> 1.1
 

  • 作者
  • Suyeol Jeon

ReactorKit

Swift CocoaPods Platform CI Codecov

ReactorKit 是一个用于创建响应式和单向流的 Swift 应用程序架构的框架。此存储库介绍了 ReactorKit 的基本概念,并描述了如何使用 ReactorKit 构建应用程序。

如果您想先看看实际代码,可以查看示例部分。要了解 ReactorKit 的功能和创建的原因,您也可以查看在 SlideShare 上的介绍性演示的幻灯片。

目录

基本概念

ReactorKit 是 Flux响应式编程 的组合。用户操作和视图状态通过可观察流传递到每一层。这些流是单向的:视图只能发出动作,而 Reactor 只能发出状态。

flow

设计目标

  • 可测试性:ReactorKit的第一个目的是将业务逻辑从视图中分离出来。这可以使代码可测试。Reactor不依赖于任何视图。只需测试Reactor和视图绑定。详细信息见测试部分。
  • 从小开始:ReactorKit不需要整个应用程序遵循单一架构。ReactorKit可以部分采用,用于一个或多个特定的视图。在现有项目中使用ReactorKit不需要重写一切。
  • 减少输入:ReactorKit注重避免为简单的事情编写复杂的代码。与其它架构相比,ReactorKit需要更少的代码。从简单开始,然后逐步扩展。

视图

视图显示数据。视图控制器和单元被视为视图。视图将用户输入绑定到动作流,并将视图状态绑定到每个UI组件。视图层中没有业务逻辑。视图只是定义了如何映射动作流和状态流。

要定义视图,只需让现有的类符合名为View的协议。然后您的类将自动获得一个名为reactor的属性。这个属性通常在视图外部设置。

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

profileViewController.reactor = UserViewReactor() // inject reactor

reactor属性发生变化时,会调用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初始化视图控制器,请使用StoryboardView协议。所有内容相同,唯一不同的是StoryboardView在视图加载后执行绑定。

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无关的层,用于管理视图的状态。Reactor的首要角色是将控制流程与视图分离。每个视图都有一个相应的Reactor,并将所有逻辑委派给它的Reactor。Reactor不依赖于视图,因此它很容易进行测试。

遵循《反应器》协议来定义反应器。此协议要求定义三种类型:ActionMutationState。还要求一个名为 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 表示视图状态。 MutationActionState 之间的桥梁。反应器通过两步将动作流转换为状态流: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() 从之前的 StateMutation 生成一个新 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() 转换每个流。有三个 transform() 函数

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
}

高级

全局状态

与Redux不同,ReactorKit没有定义全局应用状态。这意味着你可以使用任何东西来管理全局状态。你可以使用一个BehaviorSubject、一个PublishSubject甚至是一个reactor。ReactorKit不会强制要求有全局状态,因此你可以在应用程序的特定功能中使用ReactorKit。

动作 → 变更 → 状态流中没有全局状态。你应该使用transform(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))
}

然后,每次视图向reactor发送动作且currentUser发生变化时,都会发出该变更。

视图通信

你必须熟悉回调闭包或代理模式来在多个视图之间进行通信。ReactorKit建议你使用响应式扩展来实现。最常见的ControlEvent例子是UIButton.rx.tap。关键概念是将你的自定义视图视为UIButton或UILabel。

view-view

假设我们有一个ChatViewController,用于显示消息。《ChatViewController拥有一个MessageInputView。当用户在MessageInputView上点击发送按钮时,文本将被发送到ChatViewController,并且ChatViewController将绑定到reactor的动作。这是MessageInputView的响应式扩展示例

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)

测试

ReactorKit具有内置的测试功能。你将能够轻松地按照以下说明测试视图和reactor。

要测试什么

首先,你必须决定要测试什么。有两件事要测试:视图和reactor。

  • 视图
    • 动作:是不是有一个合适动作根据用户交互发送到了reactor?
    • 状态:视图属性是否已正确设置到后续状态中?
  • Reactor
    • 状态:状态是否因动作而正确更改?

视图测试

可以使用一个占位符反应器测试视图。反应器有一个名为stub的属性,可以记录操作并强制状态变化。如果一个反应器的占位符被启用,那么mutate()reduce()都不会执行。占位符有如下属性

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.isStubEnabled = 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.isStubEnabled = 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)
}

反应器测试

反应器可以独立测试。

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

有时单个操作会多次改变状态。例如,一个.refresh动作最初将state.isLoading设置为true,然后在刷新后设置为false。在这种情况下,使用currentState测试state.isLoading很困难,您可能需要使用RxTestRxExpect。以下是使用RxSwift的测试用例示例

func testIsLoading() {
  // given
  let scheduler = TestScheduler(initialClock: 0)
  let reactor = MyReactor()
  let disposeBag = DisposeBag()

  // when
  scheduler
    .createHotObservable([
      .next(100, .refresh) // send .refresh at 100 scheduler time
    ])
    .subscribe(reactor.action)
    .disposed(by: disposeBag)

  // then
  let response = scheduler.start(created: 0, subscribed: 0, disposed: 1000) {
    reactor.state.map(\.isLoading)
  }
  XCTAssertEqual(response.events.map(\.value.element), [
    false, // initial state
    true,  // just after .refresh
    false  // after refreshing
  ])
}

调度

定义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
  }
}

脉冲

Pulse只有在被修改时才会不同。以下是代码的说明,结果如下。

var messagePulse: Pulse<String?> = Pulse(wrappedValue: "Hello tokijh")

let oldMessagePulse: Pulse<String?> = message
message = "Hello tokijh"

oldMessagePulse != messagePulse // true
oldMessagePulse.value == messagePulse.value // true

当您想仅在新的值被分配时接收事件(即使它是相同的值)时使用。如alertMessage(见以下或PulseTests.swift

// Reactor
private final class MyReactor: Reactor {
  struct State {
    @Pulse var alertMessage: String?
  }

  func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    case let .alert(message):
      return Observable.just(Mutation.setAlertMessage(message))
    }
  }

  func reduce(state: State, mutation: Mutation) -> State {
    var newState = state

    switch mutation {
    case let .setAlertMessage(alertMessage):
      newState.alertMessage = alertMessage
    }

    return newState
  }
}

// View
reactor.pulse(\.$alertMessage)
  .compactMap { $0 } // filter nil
  .subscribe(onNext: { [weak self] (message: String) in
    self?.showAlert(message)
  })
  .disposed(by: disposeBag)

// Cases
reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
reactor.action.onNext(.doSomeAction)    // showAlert() is not called
reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
reactor.action.onNext(.alert("tokijh")) // showAlert() is called with `tokijh`
reactor.action.onNext(.doSomeAction)    // showAlert() is not called

示例

  • 计数器:ReactorKit最简单和基础的示例
  • GitHub 搜索:一个提供GitHub仓库搜索的简单应用程序
  • RxTodo:使用ReactorKit开发的iOS Todo应用程序
  • Cleverbot:使用Cleverbot和ReactorKit开发的iOS消息应用程序
  • Drrrible:使用ReactorKit开发的iOS版Dribbble应用(App Store
  • Passcode:iOS Passcode示例,使用RxSwift、ReactorKit和IGListKit
  • Flickr Search:一个简单应用,提供带有RxSwift和ReactorKit的Flickr照片搜索
  • ReactorKitExample
  • reactorkit-keyboard-example:使用ReactorKit架构开发键盘扩展的iOS应用示例。
  • SWHub:使用ReactorKit开发的Github客户端

依赖关系

需求

  • Swift 5
  • iOS 8
  • macOS 10.11
  • tvOS 9.0
  • watchOS 2.0

安装

Podfile

pod 'ReactorKit'

Package.swift

let package = Package(
  name: "MyPackage",
  dependencies: [
    .package(url: "https://github.com/ReactorKit/ReactorKit.git", .upToNextMajor(from: "3.0.0"))
  ],
  targets: [
    .target(name: "MyTarget", dependencies: ["ReactorKit"])
  ]
)

ReactorKit并不官方支持Carthage。

Cartfile

github "ReactorKit/ReactorKit"

大多数Carthage安装问题可以通过以下方法解决

carthage update 2>/dev/null
(cd Carthage/Checkouts/ReactorKit && swift package generate-xcodeproj)
carthage build

贡献

任何讨论和拉取请求都受欢迎💖

  • 用于开发

    $ TEST=1 swift package generate-xcodeproj
  • 用于测试

    $ swift test

社区

加入

社区项目

谁在使用ReactorKit


StyleShare Kakao Wantedly

DocTalk Constant Contact KT

Hyperconnect Toss LINE Pay

LINE Pay Kurly

你使用ReactorKit吗?请告诉我

更新日志

  • 2017-04-18
    • 将仓库名称更改为ReactorKit。
  • 2017-03-17
    • 将架构名称从RxMVVM更改为响应式架构。
    • 将所有ViewModel重命名为ViewReactors。

许可证

ReactorKit遵循MIT许可证。有关更多信息,请参阅LICENSE