Mini-Swift
Swift中Flux架构的最简化表达。
Mini旨在成为Swift应用中的第一公民:macOS、iOS和tvOS。使用Mini,您可以创建一个线程安全的应用程序,具有可预测的单向数据流,专注于真正重要的事情:构建出色的应用程序。
要求
- Xcode 10或更高版本
- Swift 5.0或更高版本
- iOS 11或更高版本
- macOS 10.13或更高版本
- tvOS 11或更高版本
安装
Swift Package Manager
- 创建一个
Package.swift
文件。
// swift-tools-version:5.0
import PackageDescription
let package = Package(
name: "MiniSwiftProject",
dependencies: [
.package(url: "https://github.com/minuscorp/Mini-Swift.git"),
],
targets: [
.target(name: "MiniSwiftProject", dependencies: ["Mini" /*, "MiniPromises, MiniTasks"*/])
]
)
- Mini附带一个裸实实现和两个外部实用程序包,以简化库的使用,这些库分别为
MiniTasks
和MiniPromises
,它们都依赖于Mini
基本或核心包。
$ swift build
Cocoapods
- 请将以下内容添加到您的
Podfile
pod "Mini-Swift"
# pod "Mini-Swift/MiniPromises"
# pod "Mini-Swift/MiniTasks"
- 我们还提供两个用于日志记录和测试的子规范
pod "Mini-Swift/Log"
pod "Mini-Swift/Test"
用法
- MiniSwift 是一个库,旨在简化 Swift 应用的基于 Flux 架构的使用。由于其基于 Flux 的特性,它严重依赖于其一些概念,如 Store、State、Dispatcher、Action、Task 和 Reducer。
状态
-
架构的最小单位基于 状态 的想法。正如其名,状态 是应用程序在某一时段的表示。
-
状态 是一个简单的
struct
,由不同的 Promise 组成,这些 Promise 包含代表当前状态的单个信息块,这可以按照以下方式实现。 -
例如
// If you're using MiniPromises
struct MyCoolState: StateType {
let cool: Promise<Bool>
}
// If you're using MiniTasks
struct MyCoolState: StateType {
let cool: Bool?
let coolTask: AnyTask
}
-
Promise
的默认内部状态是idle
。另一方面,Task
的默认内部状态也是idle
。这意味着没有任何Action
(见下文)开始对该Promise
或Task
进行操作。 -
Promise
和Task
都可以持有开发者可能认为对其实现有用的任何附加属性,例如,保留一个用于缓存使用的Date
。
let promise: Promise<Bool> = .idle()
promise.date = Date()
// Later on...
let date: Date = promise.date
let task: AnyTask = .idle()
task.date = Date()
// Later on...
let date: Date = task.date
-
《状态》的核心思想是其 不可变性,因此一旦创建,第三方对象就无法在不受架构流程控制的情况下对其进行修改。
-
如示例所示,一个
State
有一个Task
+Result
的对 通常(可以是任何对象,如果有的话),这与Task
的执行相关。在上述示例中,CoolTask
通过其Reducer
负责使用Task
结果完成Action
,并进一步提供新的State
。 -
此外,
Promise
对象统一了 状态 + 结果 对,因此它可以存储正在进行的任务的状态及其产生的关联有效负载。
操作
- “操作”是通过架构分发的信息片段。任何
struct
都可以符合“操作”协议,唯一的要求是它在每个应用程序中有一个唯一名称。
struct RequestContactsAccess: Action {
// As simple as this is.
}
-
由于“操作”不附加相关信息,因此
Mini
为用户提供两个主要的实用协议:CompletableAction
、EmptyAction
和KeyedPayloadAction
。CompletableAction
是“操作”协议的一种特殊化形式,允许用户附加Task
和当Task
成功时得到满足的某种类型的对象。
struct RequestContactsAccessResult: CompletableAction { let promise: Promise<Bool> typealias Payload = Bool }
EmptyAction
是CompletableAction
的一种特殊化,其中Payload
是Swift.Void
,这意味着它只关联一个Promise
。
struct ActivateVoucherLoaded: EmptyAction { let promise: Promise<Void> }
KeyedPayloadAction
为CompletableAction
添加一个Key
(它是Hashable
)。这是一个特殊情况,其中相同的“操作”可以生成可以分组在一起的结果,通常在Dictionary
(即搜索联系人的操作,并按他们的主电话号码分组)下。
struct RequestContactLoadedAction: KeyedCompletableAction { typealias Payload = CNContact typealias Key = String let promise: [Key: Promise<Payload?>] }
我们利用使用
struct
的优点,因此所有初始化器都是自动合成的。
示例使用
Promise
完成,但使用Task
也有等效功能。
存储
-
“存储”是决策和副作用通过传入和传出的“操作”进行的中心。一个“存储”是一个通用类,可用来继承并关联一个“状态”。
-
“存储”可以产生像任何其他
RxSwift
的Observable
一样的“状态”变化,可以被观察到。这样,一个View
或其他任何选择的对象都可以接收由某个“存储”产生的新的“状态”。 -
通过
var reducerGroup: ReducerGroup
属性,存储
减少了大量“操作”的流动。 -
“存储”的实现方式有两个泛型要求,一个
State: StateType
和一个StoreController: Disposable
。通常,“存储控制器”是一个包含执行可能被存储拦截的“操作”逻辑的类的名字,例如,一组URL请求,执行数据库查询等。 -
通过泛型特殊化,可以不需要继承“存储”即可根据每种“状态”和“存储控制器”对的组合重写
reducerGroup
变量。
extension Store where State == TestState, StoreController == TestStoreController {
var reducerGroup: ReducerGroup {
return ReducerGroup(
// Using Promises
Reducer(of: OneTestAction.self, on: dispatcher) { action in
self.state = self.state.copy(testPromise: *.value(action.counter))
},
// Using Tasks
Reducer(of: OneTestAction.self, on: dispatcher) { action in
self.state = self.state.copy(data: *action.payload, dataTask: *action.task)
}
)
}
}
- 在上面的片段中,我们有一个完整的示例,说明
存储
的工作方式。我们使用ReducerGroup
来指示“存储”将如何拦截类型为OneTestAction
的“操作”,并且每次它被拦截时,都会复制存储
的“状态”(这不是黑魔法 🧙,是通过在包中分发的Sourcery脚本来实现的)。
如果您正在使用SPM或Carthage,它们实际上并不允许与库一起分发资源,在这种情况下,我们建议您仅在每个项目中安装
Sourcery
,并使用可以从Templates
目录直接下载的模板。
- 在处理
Store
实例时,您可能会保留其reducerGroup
的强引用,这是通过使用subscribe()
方法完成的,它是一个可以被这样使用的Disposable
let bag = DisposeBag()
let store = Store<TestState, TestStoreController>(TestState(), dispatcher: dispatcher, storeController: TestStoreController())
store
.subscribe()
.disposed(by: bag)
调度器
- 架构的最后一部分是
Dispatcher
。在一个应用范围内,应该只有一个活动的Dispatcher
,从那里的每个动作都会进行分发。
let action = TestAction()
dispatcher.dispatch(action, mode: .sync)
- 一行代码就可以通知所有为此类型
Action
定义了reducer的Store
。
高级用法
-
Mini是基于请求-响应单向流程构建的。这是通过使用一对
Action
来实现的,一个用于请求改变某个State
的请求,另一个Action
用于根据操作结果突变State
。 -
这可以通过一个代码示例来说明。
使用Promise
// We define our state in first place:
struct TestState: StateType {
// Our state is defined over the Promise of an Integer type.
let counter: Promise<Int>
init(counter: Promise<Int> = .idle()) {
self.counter = counter
}
public func isEqual(to other: StateType) -> Bool {
guard let state = other as? TestState else { return false }
guard counter == state.counter else { return false }
return true
}
}
// We define our actions, one of them represents the request of a change, the other one the response of that change requested.
// This is the request
struct SetCounterAction: Action {
let counter: Int
}
// This is the response
struct SetCounterActionLoaded: Action {
let counter: Int
}
// As you can see, both seems to be the same, same parameters, initializer, etc. But next, we define our StoreController.
// The StoreController define the side-effects that an Action might trigger.
class TestStoreController: Disposable {
let dispatcher: Dispatcher
init(dispatcher: Dispatcher) {
self.dispatcher = dispatcher
}
// This function dispatches (always in a async mode) the result of the operation, just giving out the number to the dispatcher.
func counter(_ number: Int) {
self.dispatcher.dispatch(SetCounterActionLoaded(counter: number), mode: .async)
}
public func dispose() {
// NO-OP
}
}
// Last, but not least, the Store definition with the Reducers
extension Store where State == TestState, StoreController == TestStoreController {
var reducerGroup: ReducerGroup {
ReducerGroup(
// We can use Promises:
// We set the state with a Promise as .pending, someone has to fill the requirement later on. This represents the Request.
Reducer(of: SetCounterAction.self, on: self.dispatcher) { action in
guard !self.state.counter.isOnProgress else { return }
self.state = TestState(counter: .pending())
self.storeController.counter(action.counter)
},
// Next we receive the Action dispatched by the StoreController with a result, we must fulfill our Promise and notify the store for the State change. This represents the Response.
Reducer(of: SetCounterActionLoaded.self, on: self.dispatcher) { action in
self.state.counter
.fulfill(action.counter)
.notify(to: self)
}
)
}
}
使用任务
// We define our state in first place:
struct TestState: StateType {
// Our state is defined over the Promise of an Integer type.
let counter: Int?
let counterTask: AnyTask
init(counter: Int = nil,
counterTask: AnyTask = .idle()) {
self.counter = counter
self.counterTask = counterTask
}
public func isEqual(to other: StateType) -> Bool {
guard let state = other as? TestState else { return false }
guard counter == state.counter else { return false }
guard counterTask == state.counterTask else { return false }
return true
}
}
// We define our actions, one of them represents the request of a change, the other one the response of that change requested.
// This is the request
struct SetCounterAction: Action {
let counter: Int
}
// This is the response
struct SetCounterActionLoaded: Action {
let counter: Int
let counterTask: AnyTask
}
// As you can see, both seems to be the same, same parameters, initializer, etc. But next, we define our StoreController.
// The StoreController define the side-effects that an Action might trigger.
class TestStoreController: Disposable {
let dispatcher: Dispatcher
init(dispatcher: Dispatcher) {
self.dispatcher = dispatcher
}
// This function dispatches (always in a async mode) the result of the operation, just giving out the number to the dispatcher.
func counter(_ number: Int) {
self.dispatcher.dispatch(
SetCounterActionLoaded(counter: number,
counterTask: .success()
),
mode: .async)
}
public func dispose() {
// NO-OP
}
}
// Last, but not least, the Store definition with the Reducers
extension Store where State == TestState, StoreController == TestStoreController {
var reducerGroup: ReducerGroup {
ReducerGroup(
// We can use Tasks:
// We set the state with a Task as .running, someone has to fill the requirement later on. This represents the Request.
Reducer(of: SetCounterAction.self, on: dispatcher) { action in
guard !self.state.counterTask.isRunning else { return }
self.state = TestState(counterTask: .running())
self.storeController.counter(action.counter)
},
// Next we receive the Action dispatched by the StoreController with a result, we must fulfill our Task and update the data associated with the execution of it on the State. This represents the Response.
Reducer(of: SetCounterActionLoaded.self, on: dispatcher) { action in
guard self.state.rawCounterTask.isRunning else { return }
self.state = TestState(counter: action.counter, counterTask: action.counterTask)
}
)
}
}
文档
所有可用的文档都可以在此找到此处
维护者
作者 & 协作者
许可证
Mini-Swift 可在 Apache 2.0 许可证下使用。更多信息请参阅 LICENSE 文件。