随着 Combine 和 SwiftUI 的引入,我们的代码库将面临一些转型期。我们的应用程序将同时使用 Combine 和第三方响应式框架,或者同时使用 UIKit 和 SwiftUI,这使得在一段时间内保证一致的架构变得可能困难。
Spin 是一个在基于 Swift 的应用程序中构建反馈循环的工具,允许您无论底层是哪种响应式编程框架,或者您使用的是哪种苹果 UI 技术(RxSwift、ReactiveSwift、Combine 以及 UIKit、AppKit、SwiftUI),都可以使用统一的语法。
如果您已经熟悉反馈循环理论,请查看 示例应用程序。
摘要
- 变更日志
- 关于状态机
- 关于 Spin
- 构建 Spin 的多种方式
- 创建反馈的多种方式
- 反馈的生命周期
- 反馈和调度
- 关于反馈中的依赖关系
- 在基于 UIKit 或 AppKit 的应用程序中使用 Spin
- 在基于 SwiftUI 的应用程序中使用 Spin
- 在多个响应式框架中使用 Spin
- 如何让 spins 互相通信
- 示例应用程序
- 安装
- 鸣谢
变更日志
请阅读 CHANGELOG.md 以获取关于进化和重大更改的信息。
关于状态机
什么是状态机?
它是一种抽象的机器,在任何给定时间都可以处在有限数量状态中的确切一个状态。状态机在响应一些外部输入时可以从一个状态变到另一个状态。从一个状态到另一个状态的转换称为转换。状态机由其状态列表、初始状态和每个转换的条件定义。
猜猜看!一个应用程序实际上就是一个状态机。
我们只是需要找到合适的工具来实现它。这就是反馈循环发挥作用的地方。
反馈环路是一种系统能够通过使用其计算结果作为自身下一个输入来自动调节的系统。它根据给定规则不断调整此值(如电子学领域,反馈环路用于自动调节信号水平等)。
这样表述可能听起来很晦涩,与软件工程无关,但“根据某些规则调整值”正是程序(以及更广泛的应用)的使用目的!应用是我们想要调节以提供遵循精确规则的一致行为的各种状态的集合。
反馈环路是内部应用程序中托管和管理状态机的完美候选者。
关于Spin
Spin是一个工具,其唯一目的就是帮助您构建名为“Spins”的反馈环路。Spin基于三个组成部分:初始状态、多个反馈和一个reducer。为了说明每一个部分,我们将使用一个基本示例:一个“反馈环路/Spin”,从0计数到10。
- 初始状态:这是计数器启动值,即0。
- 反馈:这是我们应用于计数器的规则,以实现我们的目标。如果0 <= 计数器 < 10,我们将请求增加计数器否则请求停止它。
- reducer:这是我们的Spin的状态机。它描述了给定计数的上一个值和由反馈计算出的请求的所有可能转换。例如:如果上一个值是0且请求增加它,则新值是1;如果上一个值是1且请求增加它,则新值是2,依此类推。当反馈的请求停止时,则将上一个值作为新值。
反馈是您可以执行副作用(网络、本地I/O、UI渲染等)的唯一地方,这些副作用涉及到或修改了循环局部作用域之外的状态。反之,reducer是一个纯函数,它只能给定一个先前的值和转换请求来产生一个新值。在reducer中执行副作用是禁止的,因为这会破坏其可重复性。
在实际应用中,您可以在每个Spin中显然有多个反馈,以便于分离关注点。每个反馈都将按顺序应用于输入值。
构建Spin的多种方法
Spin提供了两种构建反馈环路的方法。这两种方法等效,选择哪一种只取决于您的偏好。
让我们通过构建一个使两个整数值趋向于平均值的Spin来尝试它们(就像一些调整立体声扬声器的左右声道音量以使它们达到相同水平的系统)。
以下示例将依赖于RxSwift,以下是相关的内容:ReactiveSwift和Combine,您将看到它们的相似之处。
我们将需要一个数据类型来表示我们的状态
struct Levels {
let left: Int
let right: Int
}
我们还需要一个数据类型来描述在级别上要执行的状态转换。
enum Event {
case increaseLeft
case decreaseLeft
case increaseRight
case decreaseRight
}
现在我们可以写出将影响每个级别的两个反馈了。
func leftEffect(inputLevels: Levels) -> Observable<Event> {
// this is the stop condition to our Spin
guard inputLevels.left != inputLevels.right else { return .empty() }
// this is the regulation for the left level
if inputLevels.left < inputLevels.right {
return .just(.increaseLeft)
} else {
return .just(.decreaseLeft)
}
}
func rightEffect(inputLevels: Levels) -> Observable<Event> {
// this is the stop condition to our Spin
guard inputLevels.left != inputLevels.right else { return .empty() }
// this is the regulation for the right level
if inputLevels.right < inputLevels.left {
return .just(.increaseRight)
} else {
return .just(.decreaseRight)
}
}
最后,要描述掌控状态转换的状态机,我们需要一个reducer。
func levelsReducer(currentLevels: Levels, event: Event) -> Levels {
guard currentLevels.left != currentLevels.right else { return currentLevels }
switch event {
case .decreaseLeft:
return Levels(left: currentLevels.left-1, right: currentLevels.right)
case .increaseLeft:
return Levels(left: currentLevels.left+1, right: currentLevels.right)
case .decreaseRight:
return Levels(left: currentLevels.left, right: currentLevels.right-1)
case .increaseRight:
return Levels(left: currentLevels.left, right: currentLevels.right+1)
}
}
构建器方法
在这种情况下,“Spinner”类是您的入口点。
let levelsSpin = Spinner
.initialState(Levels(left: 10, right: 20))
.feedback(Feedback(effect: leftEffect))
.feedback(Feedback(effect: rightEffect))
.reducer(Reducer(levelsReducer))
这样,反馈循环就建立了。接下来呢?
如果您想启动它,那么您必须订阅底层反应性流。为此,在Observable中添加了一个新的操作符“.stream(from:)”,以便连接东西,并提供一个可以订阅的Observable。
Observable
.stream(from: levelsSpin)
.subscribe()
.disposed(by: self.disposeBag)
有一个快捷函数可以直接订阅底层流。
Observable
.start(spin: levelsSpin)
.disposed(by: self.disposeBag)
例如,使用Combine以相同方式使用Spin(假设效果返回AnyPublishers):
let levelsSpin = Spinner
.initialState(Levels(left: 10, right: 20))
.feedback(Feedback(effect: leftEffect))
.feedback(Feedback(effect: rightEffect))
.reducer(Reducer(levelsReducer))
AnyPublisher
.stream(from: levelsSpin)
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
.store(in: &cancellables)
or
AnyPublisher
.start(spin: levelsSpin)
.store(in: &cancellables)
声明式方法
在这种情况下,我们利用Swift 5.1函数构建器提供类似于DSL(Domain Specific Language)的语法。
let levelsSpin = Spin(initialState: Levels(left: 10, right: 20)) {
Feedback(effect: leftEffect)
Feedback(effect: rightEffect)
Reducer(levelsReducer)
}
同样,使用Combine,考虑效果返回AnyPublishers时,语法保持不变。
let levelsSpin = Spin(initialState: Levels(left: 10, right: 20)) {
Feedback(effect: leftEffect)
Feedback(effect: rightEffect)
Reducer(levelsReducer)
}
启动Spin的方式保持不变。
创建Feedback的多种方式
如您所见,一个“反馈循环/Spin”是由多个反馈创建的。反馈是在副作用函数周围的包装结构。基本来说,副作用具有这样的签名:Stream<State> -> Stream<Event>,Stream是一个反应性流(Observable,SignalProducer或AnyPublisher)。
由于不是总是容易直接操作流,所以Spin提供了一组帮助构造函数用于反馈,允许您
- 直接接收一个状态而不是一个流(如使用示例中的
Levels
) - 提供一个谓词来过滤输入状态:`RxFeedback(effect: leftEffect, filteredBy: { $0.left > 0 })`
- 通过提供一个镜头或路径来从状态中提取子状态:`RxFeedback(effect: leftEffect, lensingOn: \.left)`
请参阅FeedbackDefinition+Default.swift以获取完整性。
反馈生命周期
存在一些典型情况,其中副作用包括异步操作(如网络调用)。如果完全相同的副作用被反复调用,而不等待先前的操作结束,会发生什么?操作是否堆叠?是否在执行新操作时取消它们?
好吧,这取决于
- .cancelOnNewState,当要处理新状态时取消之前的操作
- .continueOnNewState,当要处理新状态时允许之前的操作自然结束
根据您的需求明智地选择选项。如果reducer没有防范无序事件,则不取消之前的操作可能会导致您的状态不一致。
反馈与调度
响应式编程通常与异步执行相关联。尽管每个响应式框架都包含其自己的GCD抽象,但这始终是关于说明应在哪个调度器上执行副作用。
默认情况下,Spin将在框架创建的后台线程上执行。
然而,Spin提供了一种方法来指定Spin本身及其添加的每个反馈的调度器
Spinner
.initialState(Levels(left: 10, right: 20), executeOn: MainScheduler.instance)
.feedback(Feedback(effect: leftEffect, on: SerialDispatchQueueScheduler(qos: .userInitiated)))
.feedback(Feedback(effect: rightEffect, on: SerialDispatchQueueScheduler(qos: .userInitiated)))
.reducer(Reducer(levelsReducer))
或
Spin(initialState: Levels(left: 10, right: 20), executeOn: MainScheduler.instance) {
Feedback(effect: leftEffect)
.execute(on: SerialDispatchQueueScheduler(qos: .userInitiated))
Feedback(effect: rightEffect)
.execute(on: SerialDispatchQueueScheduler(qos: .userInitiated))
Reducer(levelsReducer)
}
当然,您仍可以在反馈函数内部自行处理调度器。
关于反馈中的依赖关系
如我们所见,Feedback是围绕副作用的一种封装。副作用,按定义,将需要一些依赖来执行其工作。例如:网络服务、一些持久化工具、加密工具等等。
然而,副作用签名不允许传递依赖关系,只允许传递状态。我们如何考虑这些依赖关系呢?
这里有三种可能的技术
1: 使用容器类型
class MyUseCase {
private let networkService: NetworkService
private let cryptographicTool: CryptographicTool
init(networkService: NetworkService, cryptographicTool: CryptographicTool) {
self.networkService = networkService
self.cryptographicTool = cryptographicTool
}
func load(state: MyState) -> AnyPublisher<MyEvent, Never> {
guard state == .loading else return { Empty().eraseToAnyPublisher() }
// use the deps here
self.networkService
.fetch()
.map { [cryptographicTool] in cryptographicTool.decrypt($0) }
...
}
}
// then we can build a Feedback with this UseCase
let myUseCase = MyUseCase(networkService: MyNetworkService(), cryptographicTool: MyCryptographicTool())
let feedback = Feedback(effect: myUseCase.load)
这种技术在概念上非常熟悉,可以与您的应用程序中现有的模式兼容。
它的缺点是强制我们在副作用中捕捉依赖时更加小心。
2: 使用反馈工厂函数
在前面的技术中,我们只使用 MyUseCase 作为依赖项的容器。它除此之外没有其他用途。我们可以通过使用接收我们的依赖项并帮助在副作用中捕获它们的函数(全局或静态)来消除它
typealias LoadEffect: (MyState) -> AnyPublisher<MyEvent, Never>
func makeLoadEffect(networkService: NetworkService, cryptographicTool: CryptographicTool) -> LoadEffect {
return { state in
guard state == .loading else return { Empty().eraseToAnyPublisher() }
networkService
.fetch()
.map { cryptographicTool.decrypt($0) }
...
}
}
// then we can build a Feedback using this factory function
let effect = makeLoadEffect(networkService: MyNetworkService(), cryptographicTool: MyCryptographicTool())
let feedback = Feedback(effect: effect)
3: 使用内置的反馈初始化器
Spin 暗含了一些用于简化依赖注入的反馈初始化器。在内部,它使用了一种从上述技术派生出的通用技术。
func loadEffect(networkService: NetworkService,
cryptographicTool: CryptographicTool,
state: MyState) -> AnyPublisher<MyEvent, Never> {
guard state == .loading else return { Empty().eraseToAnyPublisher() }
networkService
.fetch()
.map
}
// then we can build a Feedback directly using the appropriate initializer
let feedback = Feedback(effect: effect, dep1: MyNetworkService(), dep2: MyCryptographicTool())
在这三种技术中,这是一种较为简练的。它感觉有点像魔法,但实际上只是在内部使用部分化。
在基于 UIKit 或 AppKit 的应用程序中使用 Spin
尽管反馈回路可以单独存在而没有任何可视化,但在我们的开发者世界中,将其用作生成将在屏幕上渲染的 State 和处理用户发出的事件的方式更有意义。
幸运的是,将 State 作为渲染的输入并从用户交互中返回事件流的过程非常类似于反馈的定义(State -> Stream<Event>),我们已知如何处理反馈,当然有了 Spin。
由于视图是 State 的函数,渲染它将改变 UI 元素的状态。它超出了循环的局部作用域的突变:UI 确实是一个副作用。我们只需要找到一种适当的方式来将其纳入 Spin 的定义中。
一旦构建了一个 Spin,我们就可以用一个新的反馈来“装饰”它,该反馈专门用于 UI 渲染/交互。有一种特殊的 Spin 类型可用于执行此装饰:UISpin。
从全局的角度看,我们可以用这张图来展示UI环境中的反馈循环。
在一个ViewController中,假设你有一个渲染函数,如下所示:
func render(state: State) {
switch state {
case .increasing(let value):
self.counterLabel.text = "\(value)"
self.counterLabel.textColor = .green
case .decreasing(let value):
self.counterLabel.text = "\(value)"
self.counterLabel.textColor = .red
}
}
我们需要用ViewController的UISpin实例变量装饰“业务”Spin,以绑定它们的生命周期。
// previously defined or injected: counterSpin is the Spin that handles our counter business
self.uiSpin = UISpin(spin: counterSpin)
// self.uiSpin is now able to handle UI side effects
// we now want to attach the UI Spin to the rendering function of the ViewController:
self.uiSpin.render(on: self, using: { $0.render(state:) })
一旦视图准备就绪(例如在“viewDidLoad”函数中),就可以开始循环。
Observable
.start(spin: self.uiSpin)
.disposed(by: self.disposeBag)
或者更简洁的版本
self.uiSpin.start()
// the underlying reactive stream will be disposed once the uiSpin will be deinit
在循环中发送事件非常简单;只需使用emit函数即可。
self.uiSpin.emit(Event.startCounter)
在基于SwiftUI的应用中使用Spin
由于SwiftUI依赖于State和View之间的绑定概念,并负责渲染,因此连接SwiftUI Spin的方法略有不同,但更简单。
在你的视图中,你需要使用“@ObservedObject”注释SwiftUI Spin变量(SwiftUISpin是一个“ObservableObject”)
@ObservedObject
private var uiSpin: SwiftUISpin<State, Event> = {
// previously defined or injected: counterSpin is the Spin that handles our counter business
let spin = SwiftUISpin(spin: counterSpin)
spin.start()
return spin
}()
然后你可以在视图中使用uiSpin.state属性来显示数据,并使用uiSpin.emit()来发送事件
Button(action: {
self.uiSpin.emit(Event.startCounter)
}) {
Text("\(self.uiSpin.state.isCounterPaused ? "Start": "Stop")")
}
SwiftUISpin也可以用来生成SwiftUI绑定
Toggle(isOn: self.uiSpin.binding(for: \.isPaused, event: .toggle) {
Text("toggle")
}
\.isPaused是一个表示状态子状态的键路径,.toggle是在切换状态时触发的事件。
在多个响应式框架中使用Spin
如简介中所述,Spin旨在简化您应用程序中多个响应式框架的共存,以便实现更平滑的过渡。因此,您可能需要区分RxSwift Feedback和Combine Feedback,因为它们具有相同的类型名,即Feedback
。同样,对于Reducer
、Spin
、UISpin
和SwiftUISpin
也是如此。
Spin框架(SpinRxSwift、SpinReactiveSwift和SpinCombine)提供了类型别名,以区分它们的内部类型。
例如,RxFeedback
是SpinRxSwift.Feedback
的类型别名,CombineFeedback
是SpinCombine.Feedback
的类型别名。
通过使用这些类型别名,可以在同一源文件中安全地使用所有Spin版本。
所有示例应用程序都同时使用了这三个响应式框架。但高级示例应用程序最有意思,因为它在相同的源文件中使用这些框架(用于依赖注入)并利用提供的类型别名。
如何使Spin相互通信
有些用例中,两个(或更多)反馈循环需要直接通信,而不涉及现有的副作用(如UI)。
一个典型的用例可能是,您有一个处理应用程序路由并在应用程序启动时检查用户身份验证状态的反馈回路。如果用户获得授权,则显示主页,否则显示登录屏幕。当然,一旦授权,用户将使用从后端获取数据的特征,这可能导致授权问题。在这种情况下,您可能希望驱动这些功能的回路与路由回路通信,以便触发新的授权状态检查。
在设计模式中,这种需求得益于中介者。这是一个用于独立系统之间通信的多路复用对象。
在Spin中,中介者的对应物称为齿轮。一个齿轮可以连接到多个反馈,允许它们推和接收事件。
如何将反馈连接到齿轮以便它可以推进/接收来自它的事件?
首先要做的事,必须创建一个齿轮
// A Gear has its own event type:
enum GearEvent {
case authorizationIssueHappened
}
let gear = Gear<GearEvent>()
我们必须告诉来自检查授权Spin的反馈如何响应发生在齿轮中的事件
let feedback = Feedback<State, Event>(attachedTo: gear, propagating: { (event: GearEvent) in
if event == .authorizationIssueHappened {
// the feedback will emit an event only in case of .authorizationIssueHappened
return .checkAuthorization
}
return nil
})
// or with the short syntax
let feedback = Feedback<State, Event>(attachedTo: gear, catching: .authorizationIssueHappened, emitting: .checkAuthorization)
...
// then, create the Check Authorization Spin with this feedback
...
最后,我们必须告诉来自功能Spin的反馈如何将事件推进齿轮
let feedback = Feedback<State, Event>(attachedTo: gear, propagating: { (state: State) in
if state == .unauthorized {
// only the .unauthorized state should trigger en event in the Gear
return .authorizationIssueHappened
}
return nil
})
// or with the short syntax
let feedback = Feedback<State, Event>(attachedTo: gear, catching: .unauthorized, propagating: .authorizationIssueHappened)
...
// then, create the Feature Spin with this feedback
...
当功能spin处于.state.unauthorized状态时,会发生以下情况:
功能Spin:状态 = .unauthorized
↓
齿轮:传播事件 = .authorizationIssueHappened
↓
授权Spin:事件 = .checkAuthorization
↓
授权Spin:状态 = authorized/unauthorized
当然,在这种情况下,两个Spin之间必须共享齿轮。根据您的用例,您可能需要将其制作成单例。
示例应用
在Spinners组织中,您可以找到2个示例应用,演示了与RxSwift、ReactiveSwift和Combine一起使用Spin的方法。
- 一个基本的计数器应用:[UIKit版本](https://github.com/Spinners/Spin.UIKit.Demo.Basic)和[SwiftUI版本](https://github.com/Spinners/Spin.SwiftUI.Demo.Basic)
- 一个更高级的“基于网络的”应用,使用依赖注入和协调器模式(UIKit):[UIKit版本](https://github.com/Spinners/Spin.UIKit.Demo)和[SwiftUI版本](https://github.com/Spinners/Spin.SwiftUI.Demo)
安装
Swift包管理器
将此URL添加到您的依赖项
https://github.com/Spinners/Spin.Swift.git
Carthage
请将以下条目添加到您的 Cartfile 中
github "Spinners/Spin.Swift" ~> 0.20.0
然后
carthage update Spin.Swift
CocoaPods
请将以下依赖添加到您的 Podfile 中
pod 'SpinReactiveSwift', '~> 0.20.0'
pod 'SpinCombine', '~> 0.20.0'
pod 'SpinRxSwift', '~> 0.20.0'
随后,您应该能够导入 SpinCommon (基础实现),SpinRxSwift,SpinReactiveSwift 或 SpinCombine
致谢
高级演示应用程序使用 Alamofire 进行网络堆栈,使用 Swinject 进行依赖注入,使用 Reusable 进行视图实例化(UIKit 版本)以及使用 RxFlow 进行协调器模式(UIKit 版本)。
以下仓库也是灵感的来源