VueFlux 1.6.0

VueFlux 1.6.0

ra1028 维护。



VueFlux 1.6.0

  • ra1028

VueFlux

Swift 的单向数据流状态管理架构 - 受 Vuex 和 Flux 启发


Swift4 Build Status CodeBeat
CocoaPods Carthage Platform Lincense


简介

VueFlux 是 Swift 的单向数据流状态管理架构,受 Vuex 和 Flux 启发。

它支持多仓库,因此所有 ViewControllers 都有指定的仓库,并且有规则确保状态只能以可预测的方式更改。

仓库还可以接收全局派发的操作。
这使得 ViewControllers 之间无需依赖。应用程序中的共享状态也通过仓库的共享实例支持。

尽管 VueFlux 使您的项目更具生产力和代码更易读,但它也带来了更多概念和样板文件的代价。
如果您的项目是小型项目,那么您很可能会没有 VueFlux 也能正常工作。
然而,随着您项目规模的扩大,VueFlux 将是处理复杂数据流的最佳选择。

VueFlux 通过高效的响应系统接收状态更改。 VueFluxReactive 是与该架构兼容的 µ 响应式框架。
任意第三方响应式框架(例如 RxSwiftReactiveSwift 等)也可以与 VueFlux 一起使用。

VueFlux Architecture


关于 VueFlux

VueFlux通过明确划分ViewController的组成角色,创建了一种单向且可预测的流。它由以下核心概念构成。
ViewController使用响应式系统来观察状态变化。
示例代码使用VueFluxReactive,将在后面进行描述。
您可以在这里查看示例实现。

状态

这是一个仅用于约束行为和突变的协议,表示存储管理的状态。
实现状态的某些属性,并使用fileprivate访问控制保持它们为只读,如下所示。
只能通过突变进行突变,并通过计算属性发布属性。

final class CounterState: State {
    typealias Action = CounterAction
    typealias Mutations = CounterMutations

    fileprivate let count = Variable(0)
}

行为

这是分发行为的函数的代理。
它们可以具有任意操作,如异步地对后端API进行请求。
从行为函数分发的行为的类型由状态确定。

enum CounterAction {
    case increment, decrement
}
extension Actions where State == CounterState {
    func increment() {
        dispatch(action: .increment)
    }

    func decrement() {
        dispatch(action: .decrement)
    }
}

突变

这是一个表示突变状态的commit函数的协议。
通过在相同文件中实现它,可以更改fileprivate属性的状态。
在存储中实际更改状态的唯一方法是通过对突变提交行为来实现。
状态的变化必须同步进行。

struct CounterMutations: Mutations {
    func commit(action: CounterAction, state: CounterState) {
        switch action {
        case .increment:
            state.count.value += 1

        case .decrement:
            state.count.value -= 1
        }
    }
}

计算属性

这是发布状态只读属性的代理。
通过在相同文件中实现它,可以访问和发布状态的fileprivate属性。
Store中存储的状态属性只能通过这种方式访问。

extension Computed where State == CounterState {
    var countTextValues: Signal<String> {
        return state.count.signal.map { String($0) }
    }
}

仓库

仓库管理状态,也可以通过共享仓库实例管理应用程序中的共享状态。
计算属性和操作只能通过这个来访问。改变状态也是如此。
从实例成员的actions分发的操作只能修改指定状态的仓库。
另一方面,从静态成员的actions分发的操作会修改具有相同通用类型State的仓库中管理的所有状态。
在ViewController中的仓库实现如下

final class CounterViewController: UIViewController {
    @IBOutlet private weak var counterLabel: UILabel!

    private let store = Store<CounterState>(state: .init(), mutations: .init(), executor: .queue(.global()))

    override func viewDidLoad() {
        super.viewDidLoad()

        store.computed.countTextValues.bind(to: counterLabel, \.text)
    }

    @IBAction func incrementButtonTapped(sender: UIButton) {
        store.actions.increment()  // Store<CounterState>.actions.increment()
    }

    @IBAction func decrementButtonTapped(sender: UIButton) {
        store.actions.decrement()  // Store<CounterState>.actions.decrement()
    }
}

关于VueFluxReactive

VueFluxReactive是一个用于观察状态变化的微反应系统。
它是为了替换现有的反应框架,如RxSwift和ReactiveSwift,它们虽然功能强大但学习成本和入门成本很高。
但当然,VueFlux也可以与这些框架一起使用,因为VueFluxReactive是分离的。
VueFluxReactive由以下原始组件构成。

接受器

该类型有一种生成信号的方法。
可以向接受器发送值并通过观察生成的信号接收它。
从接受器生成的信号不保留最新值。
在实际应用中,它用于从State向ViewController发送命令(如显示另一个ViewController)。
不能递归地传递值.

let sink = Sink<Int>()
let signal = sink.signal

signal.observe { print($0) }

sink.send(value: 100)

// prints "100"

信号

一个推力驱动的流,在时间上发送值变化。
值将同时发送到所有注册的观察者。
所有值的变化都是通过这个原始组件进行的。

let sink = Sink<Int>()
let signal = sink.signal

signal.observe { print("1: \($0)") }
signal.observe { print($2: \($0)") }

sink.send(value: 100)
sink.send(value: 200)

// prints "1: 100"
// prints "2: 100"
// prints "1: 200"
// prints "2: 200"

变量

变量表示一个线程安全的可变值,允许通过它生成的信号来观察它的变化。
观察开始时,信号将发送最新值。所有值更改在此之后都会发送。
不能递归地传递值.

let variable = Variable(0)

variable.signal.observe { print($0) }

variable.value = 1

print(variable.value)

variable.signal.observe { print($0) }

/// prints "0"
/// prints "1"
/// prints "1"
/// prints "1"

常量

这是一种用于使变量只读的包装器。
从变量生成的常量反映了其变量的变化。
就像变量一样,最新值和值更改都通过信号转发。但常量不允许直接更改。

let variable = Variable(0)
let constant = variable.constant

constant.signal.observe { print($0) }

variable.value = 1

print(constant.value)

constant.signal.observe { print($0) }

/// prints "0"
/// prints "1"
/// prints "1"
/// prints "1"

高级用法

执行器

执行器确定了函数的执行上下文,例如在主线程上执行、在全局队列上执行等。
一些上下文是默认内置的。

  • 立即
    立即异步执行函数。

  • mainThread
    如果执行线程是主线程,则立即和同步执行。否则,将其排队到主队列。

  • queue(_ dispatchQueue: DispatchQueue)
    所有函数都被排队到给定的dispatch队列。

在以下情况下,存储将提交操作到全局队列上的变更。

let store = Store<CounterState>(state: .init(), mutations: .init(), executor: .queue(.global()))

如果您观察到如下情况,观察函数将在全局后台队列上执行。

store.computed.valueSignal
    .observe(on: .queue(.global(qos: .background)))
    .observe { value in
        // Executed on global background queue
}

信号运算符

VueFluxReactive限制了功能性方法AMAP。
然而,包含了一些便利的最小运算符。
这些运算符将信号转换为新信号,由运算符生成,这意味着信号的不变性得到保持。

map
map运算符用于转换信号的值。

let sink = Sink<Int>()
let signal = sink.signal

signal
    .map { "Value is \($0)" }
    .observe { print($0) }

sink.send(value: 100)
sink.send(value: 200)

// prints "Value is 100"
// prints "Value is 200"

observe(on:)
在给定的执行器上下文中转发所有值。

let sink = Sink<Int>()
let signal = sink.signal

signal
    .observe(on: .mainThread)
    .observe { print("Value: \($0), isMainThread: \(Thread.isMainThread)") }

DispatchQueue.global().async {
    sink.send(value: 100)    
    sink.send(value: 200)
}

// prints "Value: 100, isMainThread: true"
// prints "Value: 200, isMainThread: true"

可销毁的

可销毁的表示可以销毁的东西,通常注销已注册到信号的观察者。

let disposable = signal.observe { value in
    // Not executed after disposed.
}

disposable.dispose()

DisposableScope

DisposableScope是Disposable的资源管理器。
在析构或释放时,这将终止所有添加的Disposable。
例如,当具有DisposableScope属性的所有ViewController被 dismissal 时,所有Disposable将被终止。

var disposableScope: DisposableScope? = DisposableScope()

disposableScope += signal.observe { value in
    // Not executed after disposableScope had deinitialized.
}

disposableScope = nil  // Be disposed

区域观察

在观察中,您可以将AnyObject作为duringScopeOf:的参数。
当对象被析构时,观察Signal的观察函数将被丢弃。

signal.observe(duringScopeOf: self) { value in
    // Not executed after `self` had deinitialized.
}

绑定

绑定确保目标对象的值更新为通过Signal接收到的最新值。
目标对象被析构后,绑定不再有效。
绑定默认在主线程上工作。

闭包绑定。

text.signal.bind(to: label) { label, text in
    label.text = text
}

智能键路径绑定。

text.signal.bind(to: label, \.text)

Binder

extension UIView {
    func setHiddenBinder(duration: TimeInterval) -> Binder<Bool> {
        return Binder(target: self) { view, isHidden in
            UIView.transition(
              with: view,
              duration: duration,
              options: .transitionCrossDissolve,
              animations: { view.isHidden = isHidden }
            )
        }
    }
}

isViewHidden.signal.bind(to: view.setHiddenBinder(duration: 0.3))

共享存储

您应该创建Store的共享实例来管理应用程序中共享的状态。
虽然您可以将它定义为全局变量,但优雅的方法是覆盖Store并定义一个静态成员shared

final class CounterStore: Store<CounterState> {
    static let shared = CounterStore()

    private init() {
        super.init(state: .init(), mutations: .init(), executor: .queue(.global()))
    }
}

全局分发

VueFlux还可以作为全局事件总线。
如果您从Store的静态成员 actions 调用函数,则影响具有相同State泛型类型的所有Store中的状态。

let store = Store<CounterState>(state: .init(), mutations: .init(), executor: .immediate)

print(store.computed.count.value)

Store<CounterState>.actions.increment()

print(store.computed.count.value)

// prints "0"
// prints "1"

要求

  • Swift 4.1+
  • macOS 10.9+
  • iOS 9.0+
  • watchOS 2.0+
  • tvOS 9.0+

安装

CocoaPods

如果使用 VueFlux 和 VueFluxReactive,请在您的 Podfile 中添加以下内容

use_frameworks!

target 'TargetName' do
  pod 'VueFluxReactive'
end

或者如果使用第三方 Reactive 框架

use_frameworks!

target 'TargetName' do
  pod 'VueFlux'
  # And reactive framework you like
end

然后运行

pod install

Carthage

在您的 Cartfile 中添加以下内容

github "ra1028/VueFlux"

然后运行

carthage update

示例项目


贡献

欢迎fork并提交pull request。

在提交pull request之前,请确保您已经通过了包含的测试。
如果您的pull request 包含新功能,请为它编写测试用例。


授权

VueFlux和VueFluxReactive在MIT许可下发布。