Platform Language Carthage Version License CI Status

Prex 是一个框架,它使得使用 MVP 架构的单向数据流应用程序成为可能。

概念

Prex 代表 Presenter + Flux,因此它是 Flux 和 MVP 架构的结合。此外,Prex 中未使用响应式框架。为了将状态反映到视图,使用了 被动视图模式。Flux 在 Presenter 的背后使用。数据流是单向的,如下图所示。

如果您使用 Prex,您必须实现以下组件。

状态 (State)

状态 (State) 具有在视图 (View) 和 Presenter 中使用的属性。

struct CounterState: State {
    var count: Int = 0
}

动作 (Action)

动作 (Action) 代表您应用程序的内部 API。例如,如果您想增加 CounterState 的计数,请将 Action.increment 分发到 Dispatcher。

enum CounterAction: Action {
    case increment
    case decrement
}

突变 (Mutation)

突变 (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

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) 使用 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

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 是具有泛型参数的类。您可以像这样创建 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)
    }
}

1. 反射状态测试

此测试从分发一个 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)
}

2. 创建动作测试

此测试从调用 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

您可以使用 Playground 尝试 Prex 计数器示例!打开 Prex.xcworkspace 并构建 Prex-iOS。最后,您可以在 Playground 中手动运行。

要求

安装

Carthage

如果您使用 Carthage,只需将 Prex 添加到您的 Cartfile

github "marty-suzuki/Prex"

CocoaPods

Prex 可通过 CocoaPods 获得。要安装它,只需将以下行添加到您的 Podfile

pod 'Prex'

Swift Package Manager

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 文件。