Prex 是一个框架,它通过 MVP 架构使得单向数据流应用程序成为可能。
概念
Prex 代表的是 Presenter 和 Flux,因此它是 Flux 和 MVP 架构的结合。此外,Prex 中不使用响应式框架。为了将状态反映到视图中,使用 被动视图模式。Flux 被用于 Presenter 的后面。数据流是单向的,就像以下图示。
如果您使用 Prex,则必须实现以下组件。
状态
状态具有用于在视图和展示器中使用的属性。
struct CounterState: State {
var count: Int = 0
}
操作
操作代表了应用程序的内部 API。例如,如果您想增加 CounterState 的计数器,向 Dispatcher 发送 Action.increment。
enum CounterAction: Action {
case increment
case decrement
}
变异
变异允许使用操作来变异状态。
struct CounterMutation: Mutation {
func mutate(action: CounterAction, state: inout CounterState) {
switch action {
case .increment:
state.count += 1
case .decrement:
state.count -= 1
}
}
}
演示者
演示者起着在视图和Flux组件之间连接的作用。如果你想访问副作用(API访问等),必须在演示者中访问它们。最后,使用演示者.dispatch(_:)
分派这些结果。
extension Presenter where Action == CounterAction, State == CounterState {
func increment() {
dispatch(.increment)
}
func decrement() {
if state.count > 0 {
dispatch(.decrement)
}
}
}
视图
视图通过视图.reflect(change:)
显示状态。当状态更改时,由演示者调用。此外,它通过用户交互调用演示者方法。
final class CounterViewController: UIViewController {
private let counterLabel: UILabel
private lazy var presenter = Presenter(view: self,
state: CounterState(),
mutation: CounterMutation())
@objc private func incrementButtonTap(_ button: UIButton) {
presenter.increment()
}
@objc private func decrementButtonTap(_ button: UIButton) {
presenter.decrement()
}
}
extension CounterViewController: View {
func reflect(change: StateChange<CounterState>) {
if let count = change.count?.value {
counterLabel.text = "\(count)"
}
}
}
您只能从StateChange.changedProperty(for:)
获取状态中更改的指定值。
高级用法
共享存储
存储和分派器的初始化器不是公开访问级别。但你可以使用Flux
来初始化它们,并用演示者.init(view:flux:)
注入。
这是一个共享Flux组件的示例。
extension Flux where Action == CounterAction, State == CounterState {
static let shared = Flux(state: CounterState(), mutation: CounterMutation())
}
或者
enum SharedFlux {
static let counter = Flux(state: CounterState(), mutation: CounterMutation())
}
像这样注入Flux
。
final class CounterViewController: UIViewController {
private lazy var presenter = {
let flux = Flux<CounterAction, CounterState>.shared
return Presenter(view: self, flux: flux)
}()
}
演示者子类
演示者是一个具有泛型参数的类。你可以创建演示者子类,如下所示。
final class CounterPresenter: Presenter<CounterAction, CounterState> {
init<T: View>(view: T) where T.State == CounterState {
let flux = Flux(state: CounterState(), mutation: CounterMutation())
super.init(view: view, flux: flux)
}
func increment() {
dispatch(.increment)
}
func decrement() {
if state.count > 0 {
dispatch(.decrement)
}
}
}
测试
我将解释如何使用Prex进行测试。在本文档中关注两个测试用例。
1. 反射状态测试 | 2. 创建操作测试 |
---|---|
![]() |
![]() |
两个测试都需要视图初始化一个表示器。你可以创建类似于这样的 MockView。
final class MockView: View {
var refrectParameters: ((StateChange<CounterState>) -> ())?
func reflect(change: StateChange<CounterState>) {
refrectParameters?(change)
}
}
1. 反射状态测试
这个测试从派发一个操作开始。操作传递给变异,变异用接收到的操作改变状态。存储库通知状态的更改,表示调用视图的反射方法来反射状态。最后,通过视图的反射方法参数接收状态。
这是一个示例测试代码。
func test_presenter_calls_reflect_of_view_when_state_changed() {
let view = MockView()
let flux = Flux(state: CounterState(), mutation: CounterMutation())
let presenter = Presenter(view: view, flux: flux)
let expect = expectation(description: "wait receiving ValueChange")
view.refrectParameters = { change in
let count = change.changedProperty(for: \.count)?.value
XCTAssertEqual(count, 1)
expect.fulfill()
}
flux.dispatcher.dispatch(.increment)
wait(for: [expect], timeout: 0.1)
}
2. 创建操作测试
这个测试以调用表示器方法作为模拟用户交互开始。表示器访问副作用,最终从结果创建一个操作。该操作被派发到分发器。最后,通过分发器的注册回调接收操作。
这是一个示例测试代码。
func test_increment_method_of_presenter() {
let view = MockView()
let flux = Flux(state: CounterState(), mutation: CounterMutation())
let presenter = Presenter(view: view, flux: flux)
let expect = expectation(description: "wait receiving ValueChange")
let subscription = flux.dispatcher.register { action in
XCTAssertEqual(action, .increment)
expect.fulfill()
}
presenter.increment()
wait(for: [expect], timeout: 0.1)
flux.dispatcher.unregister(subscription)
}
一个额外的例子
你可以这样测试状态变异。
func test_mutation() {
var state = CounterState()
let mutation = CounterMutation()
mutation.mutate(action: .increment, state: &state)
XCTAssertEqual(state.count, 1)
mutation.mutate(action: .decrement, state: &state)
XCTAssertEqual(state.count, 0)
}
示例
项目
您可以通过 GitHub 仓库搜索应用程序 示例 尝试 Prex。打开 PrexSample.xcworkspace 并运行它!
沙盒
您可以在沙盒中尝试 Prex 计数器示例!打开 Prex.xcworkspace 并构建 Prex-iOS
。最后,您可以手动在沙盒中运行。
需求
- Xcode 9.4.1 或更高版本
- iOS 10.0 或更高版本
- tvOS 10.0 或更高版本
- macOS 10.10 或更高版本
- watchOS 3.0 或更高版本
- Swift 4.1 或更高版本
安装
Carthage
如果您正在使用 Carthage,只需将 Prex 添加到您的 Cartfile
中。
github "marty-suzuki/Prex"
CocoaPods
Prex 可以通过 CocoaPods 获取。要安装它,只需将以下行添加到您的 Podfile 中。
pod 'Prex'
Swift 包管理器
Prex 通过 Swift 包管理器
提供。只需将此存储库的 URL 添加到您的 Package.swift
。
dependencies: [
.package(url: "https://github.com/marty-suzuki/Prex.git", from: "0.2.0")
]
受以下单向数据流框架启发
- VueFlux 由 @ra1028 提供
- ReactorKit 由 @devxoul 提供
作者
marty-suzuki, [email protected]
许可证
Prex 可在 MIT 许可证下获取。有关更多信息,请参阅 LICENSE 文件。