ReactorKit 是一个用于创建响应式和单向流的 Swift 应用程序架构的框架。此存储库介绍了 ReactorKit 的基本概念,并描述了如何使用 ReactorKit 构建应用程序。
如果您想先看看实际代码,可以查看示例部分。要了解 ReactorKit 的功能和创建的原因,您也可以查看在 SlideShare 上的介绍性演示的幻灯片。
目录
基本概念
ReactorKit 是 Flux 和 响应式编程 的组合。用户操作和视图状态通过可观察流传递到每一层。这些流是单向的:视图只能发出动作,而 Reactor 只能发出状态。
设计目标
- 可测试性: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不依赖于视图,因此它很容易进行测试。
遵循《反应器》协议来定义反应器。此协议要求定义三种类型:Action
、Mutation
和 State
。还要求一个名为 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
表示视图状态。 Mutation
是 Action
和 State
之间的桥梁。反应器通过两步将动作流转换为状态流:mutate()
和 reduce()
。
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()
转换每个流。有三个 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。
假设我们有一个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
很困难,您可能需要使用RxTest或RxExpect。以下是使用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客户端
依赖关系
- RxSwift >= 5.0
需求
- 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,在RxSwift Slack
- 韩文:加入#reactorkit,在Swift Korea Slack
社区项目
谁在使用ReactorKit
你使用ReactorKit吗?请告诉我!
更新日志
- 2017-04-18
- 将仓库名称更改为ReactorKit。
- 2017-03-17
- 将架构名称从RxMVVM更改为响应式架构。
- 将所有ViewModel重命名为ViewReactors。
许可证
ReactorKit遵循MIT许可证。有关更多信息,请参阅LICENSE。