ReRxSwift
目录
简介
如果您还不知道,这两个框架都很棒
- ReSwift 是一个小型框架,它以 Swift 语言实现了 单向数据流架构。它受到了 JavaScript 实现该架构的启发: Redux。
- RxSwift 是 ReactiveX 的 Swift 实现。在其他事物中,它允许您轻松地将属性绑定到用户界面元素。
使用这两个框架,您可以构建非常出色的应用程序架构。RxSwift 允许您将应用程序状态绑定到 UI,而 ReSwift 则在响应操作时发布状态更新。但我们更进一步。
类似于 react-redux,ReRxSwift 允许您创建具有 props
和 actions
的视图控制器。视图控制器从它们的 props
中读取所有需要的数据(而不是直接从状态中),并通过调用 actions
中定义的回调来更改数据(而不是直接分发 ReSwift 操作)。这有一些优点
- 关注点分离更佳。更容易理解你的控制器到底做什么,以及它使用什么数据。换句话说,这便于进行局部推理。
- 单元测试。由于关注点分离,你可以轻松地单元测试视图控制器,一直到接口构建器连接。
- 更好的可重用性。重用你的视图控制器就像指定从状态到
props
和从ReSwift动作到actions
的不同映射。 (请参阅下面的“开放问题”章节。) - 快速原型设计。你可以轻松地使用模拟的
props
和actions
,以便你得到一个工作的UI层原型。不编写任何应用程序的业务逻辑,你可以这样实现你的表示层,使得用真实状态和动作替换模拟变得非常简单。
安装
使用这个库最简单的方法是通过Cocoapods或Carthage。对于CocoaPods,在你的Podfile
中添加以下内容
pod 'ReRxSwift', '~> 2.0'
对于Carthage,在你的Cartfile
中添加以下内容
github "svdo/ReRxSwift" ~> 2.0
使用
本节假设存在一个全局变量store
,它包含你的应用程序的商店,并且它的类型是Store<AppState>
。你有一个控制器来管理一个文本框;文本框显示状态中的某个值,在editingDidEnd
时,你触发一个动作将文本框的内容存储回状态。要使用ReRxSwift为你的控制器MyViewController
,你可以按照以下步骤操作。
-
为你的控制器创建一个扩展,使其成为
Connectable
,定义你的视图控制器需要的Props
和Actions
extension MyViewController: Connectable { struct Props { let text: String } struct Actions { let updatedText: (String) -> Void } }
-
定义你的状态如何映射到上述
Props
类型private let mapStateToProps = { (appState: AppState) in return MyViewController.Props( text: appState.content ) }
-
定义发出的动作
private let mapDispatchToActions = { (dispatch: @escaping DispatchFunction) in return MyViewController.Actions( updatedText: { newText in dispatch(SetContent(newContent: newText)) } ) }
-
定义连接并将其连接起来
class MyViewController: UIViewController { @IBOutlet weak var textField: UITextField! let connection = Connection( store: store, mapStateToProps: mapStateToProps, mapDispatchToActions: mapDispatchToActions ) override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) connection.connect() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) connection.disconnect() } }
-
绑定文本框的文本,使用Swift 4键路径引用
Props
中的text
属性override func viewDidLoad() { super.viewDidLoad() connection.bind(\Props.text, to: textField.rx.text) }
-
调用动作
@IBAction func editingChanged(_ sender: UITextField) { actions.updatedText(sender.text ?? "") }
这是样本应用程序中的SimpleTextFieldViewController
。该视图控制器带有一套完整的单元测试:SimpleTextFieldViewControllerSpec
。
API
以下是API最重要组件的高级描述。还有ReRxSwift的完整API文档可用于。
可连接
这是您的视图控制器必须遵守的协议。它要求您添加一个 connection
属性。它提供您可以在视图控制器中使用的 props
和 actions
。通常,您可以这样声明 connection
class MyViewController: Connectable {
let connection = Connection(
store: store,
mapStateToProps: mapStateToProps,
mapDispatchToActions: mapDispatchToActions
)
}
有关更多信息,请参考 Connection
构造函数文档。
props
这包含了您使用 mapStateToProps
创建的 Props
对象。换句话说:它包含了视图控制器使用的一切数据,自动从您的应用程序状态中提取出来。当在 Connection
中使用 bind
方法时,您可能不需要直接使用此 props
属性。
actions
这包含了您使用 mapDispatchToProps
创建的 Actions
对象。换句话说:它指定在调用您 actions
定义的回调时,必须派发哪个 ReSwift 行动。
Connection
Connection
负责将您应用程序状态映射到视图控制器的 props
,以及在视图控制器 actions
中调用函数时派发映射的动作。
Constructor(store, mapStateToProps, mapDispatchToActions)
要创建您的 Connection
实例,您需要用三个参数来构造它
- store:您的应用程序的 ReSwift 存储。
- mapStateToProps:一个函数,它从您应用程序的状态中获取值并将它们放入视图控制器中的
props
对象。这将您的应用程序状态与视图控制器数据解耦。 - mapDispatchToActions:一个函数,指定您视图控制器可以调用哪些操作,以及对每个操作,ReSwiftaction 需要被发送。
connect()
调用此方法将导致连接订阅 ReSwift 存储并接收应用程序状态更新。从您的视图控制器的 viewWillAppear
或 viewDidAppear
方法中调用此方法。
disconnect()
调用此方法会导致连接从 ReSwift 存储中取消订阅。从您的视图控制器的 viewWillDisappear
或 viewDidDisappear
中调用此方法。
subscribe(keyPath, onNext)
订阅您视图控制器 props
中的条目,这意味着每当该条目发生变化时,将调用所给的 onNext
处理器。
bind(keyPath, to, mapping)
这个函数将您的视图控制器中的 props
中的一个条目绑定到一个启用了 RxSwift 的用户界面元素上,因此每当您的 props
发生变化时,用户界面元素将相应地自动更新。
bind
函数接受以下参数
- keyPath:指向您要在
props
中绑定的用户界面元素的那个元素的(Swift 4)key path。 - to:RxSwift 反应式属性包装器,例如
textField.rx.text
或progressView.rx.progress
。 - 映射:大多数的bind变体(但不是全部)允许你提供一个映射函数。这个映射函数被应用于指定
keyPath
处的props
元素。你可以用这个例子来进行类型转换:你的props
包含一个作为Float
的值,但UI元素需要它是一个String
。指定映射{ String($0) }
将处理这个问题。SteppingUpViewController.swift
包含了将Float
值映射到分段控制所选索引的映射函数示例。
仅为了更好理解:存在多种bind
函数的变体。它们都是以下简化代码的变体
self.props
.asObservable()
.distinctUntilChanged { $0[keyPath: keyPath] == $1[keyPath: keyPath] }
.map { $0[keyPath: keyPath] }
.bind(to: observer)
.disposed(by: disposeBag)
示例应用
Example
文件夹包含以下示例
- SimpleTextField:ReRxSwift最基本的使用场景,包含一个其值已绑定并有一个动作的文本框。
- SteppingUp:具有多个绑定值和动作的使用场景,同时展示了在绑定时如何转换值。
- TableAndCollection:展示了如何结合使用ReRxSwift、RxSwift、RxCocoa和RxDataSources,以拥有非常简单的可自动动画更新的表格/集合视图。
常见问题解答(FAQ)
props
没有更新?
当应用状态变化时,我的这发生在你忘记在你的视图控制器viewWillAppear
或viewDidAppear
方法中调用connection.connect()
的时候。趁此机会,你可能还想验证你也在viewWillDisappear
或viewDidDisappear
中调用了connection.disconnect()
。
connection.bind()
时,我得到了编译器错误?
当我调用调用bind
时,您需要将一个键路径传递给props
对象中的元素。由于ReRxSwift确保只有当该元素实际发生变化时才会触发,它会将它的值与之前的值进行比较。这意味着您的props
对象中的元素需要实现Equatable
协议。当然,简单类型已经实现了Equatable
,但在绑定表格视图项或集合视图项时,您需要确保类型实现了Equatable
。
另一个常见的错误来源是您的props
中元素的类型与用户界面元素预期的不完全匹配。例如,您将stepValue
(一个Double
类型)绑定到一个步进器上来,但您的props
包含了Float
。在这些情况下,您可以将映射函数作为bind(_:to:mapping:)
的第三个参数传递,以将props
元素转换为期望的类型。请参见SteppingUpViewController.swift
以获取示例。
我已经仔细检查了一切,但我仍然收到错误!
请在GitHub上创建一个新问题,因为您可能遇到了一个错误。(但请确保您的Props
类型中的所有内容都是Equatable
!)