Prex 是一个框架,它使得使用 MVP 架构的单向数据流应用程序成为可能。
Prex 代表 Presenter + Flux,因此它是 Flux 和 MVP 架构的结合。此外,Prex 中未使用响应式框架。为了将状态反映到视图,使用了 被动视图模式。Flux 在 Presenter 的背后使用。数据流是单向的,如下图所示。
如果您使用 Prex,您必须实现以下组件。
状态 (State) 具有在视图 (View) 和 Presenter 中使用的属性。
struct CounterState: State {
var count: Int = 0
}
动作 (Action) 代表您应用程序的内部 API。例如,如果您想增加 CounterState 的计数,请将 Action.increment 分发到 Dispatcher。
enum CounterAction: Action {
case increment
case decrement
}
突变 (Mutation) 允许使用动作 (Action) 改变状态 (State)。
struct CounterMutation: Mutation {
func mutate(action: CounterAction, state: inout CounterState) {
switch action {
case .increment:
state.count += 1
case .decrement:
state.count -= 1
}
}
}
Presenter 的作用是在视图 (View) 和 Flux 组件之间建立连接。如果您想访问副作用(API 访问等),您必须在 Presenter 中访问它们。最后,您可以使用 Presenter.dispatch(_:)
分发这些结果。
extension Presenter where Action == CounterAction, State == CounterState {
func increment() {
dispatch(.increment)
}
func decrement() {
if state.count > 0 {
dispatch(.decrement)
}
}
}
视图 (View) 使用 View.reflect(change:)
显示状态 (State)。当状态 (State) 发生更改时,Presenter 会调用它。此外,它通过用户交互调用 Presenter 方法。
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:)
中获取状态 (State) 中已更改的指定值。
Store 和 Dispatcher 的初始化器不是公共访问级别。但是您可以使用 Flux
初始化它们,并使用 Presenter.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)
}()
}
Presenter 是具有泛型参数的类。您可以像这样创建 Presenter 子类。
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. 创建动作测试 |
---|---|
![]() |
![]() |
这两个测试都需要 View 来初始化 Presenter。您可以像这样创建 MockView。
final class MockView: View {
var refrectParameters: ((StateChange<CounterState>) -> ())?
func reflect(change: StateChange<CounterState>) {
refrectParameters?(change)
}
}
此测试从分发一个 Action 开始。Action 被传递给 Mutation,Mutation 使用接收到的 Action 改变 State。Store 通知状态 (State) 的更改,Presenter 调用 View 的 reflect 方法来反射状态 (State)。最后,通过 View 的 reflect 方法参数接收状态 (State)。
这是一个示例测试代码。
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)
}
此测试从调用 Presenter 方法作为虚拟用户交互开始。Presenter 访问副作用,并最终从该结果创建一个 Action。该 Action 被分发到 Dispatcher。最后,通过 Dispatcher 的 register 回调接收 Action。
这是一个示例测试代码。
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)
}
您可以像这样测试改变状态 (State)。
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 并运行它!
您可以使用 Playground 尝试 Prex 计数器示例!打开 Prex.xcworkspace 并构建 Prex-iOS
。最后,您可以在 Playground 中手动运行。
如果您使用 Carthage,只需将 Prex 添加到您的 Cartfile
github "marty-suzuki/Prex"
Prex 可通过 CocoaPods 获得。要安装它,只需将以下行添加到您的 Podfile
pod 'Prex'
Prex 可通过 Swift Package Manager
获得。只需将此仓库的 URL 添加到您的 Package.swift
。
dependencies: [
.package(url: "https://github.com/marty-suzuki/Prex.git", from: "0.2.0")
]
marty-suzuki, s1180183@gmail.com
Prex 在 MIT 许可证下可用。有关更多信息,请参阅 LICENSE 文件。