Prex 0.3.0

Prex 0.3.0

Taiki Suzuki 维护。



Prex 0.3.0

Platform Language Carthage Version License CI Status

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")
]

受以下单向数据流框架启发

作者

marty-suzuki, [email protected]

许可证

Prex 可在 MIT 许可证下获取。有关更多信息,请参阅 LICENSE 文件。