Feedbacks 是一个工具,旨在帮助你在 Swift 应用程序中构建可靠且可组合的功能。每个功能都基于一个“系统”(System),它代表执行此功能所需的所有软件组件之间的通信。
Feedbacks 完全基于一种声明式语法,用于描述你的系统的行为。如果你喜欢 SwiftUI,你也会喜欢 Feedbacks,因为它语法提倡组合模式和修饰符。
一个系统依赖于三件事:
这是一个系统,它根据初始音量和目标音量来调节扬声器的音量。
struct VolumeState: State { let value: Int }
struct IncreaseEvent: Event {}
struct DecreaseEvent: Event {}
let targetedVolume = 15
let system = System {
InitialState {
VolumeState(value: 10)
}
Feedbacks {
Feedback(on: VolumeState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
if state.value >= targetedVolume {
return Empty().eraseToAnyPublisher()
}
return Just(IncreaseEvent()).eraseToAnyPublisher()
}
Feedback(on: VolumeState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
if state.value <= targetedVolume {
return Empty().eraseToAnyPublisher()
}
return Just(DecreaseEvent()).eraseToAnyPublisher()
}
}
Transitions {
From(VolumeState.self) { state in
On(IncreaseEvent.self, transitionTo: VolumeState(value: state.value + 1))
On(DecreaseEvent.self, transitionTo: VolumeState(value: state.value - 1))
}
}
}
让我们分解一下。
状态机是一个抽象机器,在任何给定时间都只能处于有限数量的状态之一。 状态机可以响应一些外部事件从一个状态更改为另一个状态。 从一个状态到另一个状态的改变称为转换。 状态机由其状态列表、其初始状态以及每个转换的条件定义。
状态机可以完美地描述应用程序中的一个功能。 状态机的好处在于它们的可预测性。 从给定状态和给定事件出发,转换将始终返回相同的状态。
要定义状态机,我们需要定义三件事:状态、事件和转换。 在“扬声器音量示例”中,我们定义了 1 个状态:当前音量,以及 2 个事件:1 个用于增加音量,1 个用于降低音量。 然后,你所要做的就是描述转换及其执行的条件。
这是上述系统中描述的状态机。
一方面,定义转换是关于描述应用程序中什么是不可变的,什么是不能根据外部条件改变的,什么是高度可预测和可测试的。
另一方面,应用程序通常需要访问来自网络或数据库的数据,这取决于系统之外的条件(文件系统、数据可用性等)。 这些副作用可以在 Feedbacks 中定义。
我们可以总结为:任何改变系统状态的东西都是转换,任何访问系统外部状态的东西都是副作用。
Feedback 是一种对副作用的语义封装。
在“扬声器音量示例”中,在某个时刻我们需要访问一个外部属性:目标音量。 它对于系统来说是外部的,因为它是一个可以独立于系统设置和存储的变量。 对它的访问必须隔离在副作用中。
副作用是一个函数,它通过产生可能触发转换的事件来对输入状态做出反应。 由于副作用可以是异步的(例如,从网络获取数据),因此它应该返回一个事件的 Publisher。
在我们的示例中,一个反馈负责增加音量,另一个负责降低音量。 每次转换生成新状态时,都会执行这两个反馈。
线程对于创建一个好的响应式应用程序非常重要。 调度器是 Combine 处理线程的方式,通过在调度队列、操作队列或 RunLoop 上切换反应流的部分。
Feedbacks 的声明式语法允许通过简单地应用修饰符(就像你使用 SwiftUI 更改 frame 一样)来更改系统的行为。 修改副作用的调度就像调用 .execute(on:)
修饰符一样简单。
Feedbacks {
Feedback(on: LoadingState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
performLongRunningOperation()
.map { FinishedLoadingEvent() }
.eraseToAnyPublisher()
}
.execute(on: DispatchQueue(label: "A background queue"))
}
就像在 SwiftUI 中一样,修饰符可以应用于容器
Feedbacks {
Feedback(on: LoadingState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
...
}
Feedback(on: SelectedState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
...
}
}
.execute(on: DispatchQueue(label: "A background queue"))
这两个副作用都将在后台队列中执行。
它也适用于转换
Transitions {
From(VolumeState.self) { state in
On(IncreaseEvent.self, transitionTo: VolumeState(value: state.value + 1))
On(DecreaseEvent.self, transitionTo: VolumeState(value: state.value - 1))
}
}.execute(on: DispatchQueue(label: "A background queue"))
或者整个系统
System {
InitialState {
VolumeState(value: 10)
}
Feedbacks {
Feedback(on: VolumeState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
if state.value >= targetedVolume {
return Empty().eraseToAnyPublisher()
}
return Just(IncreaseEvent()).eraseToAnyPublisher()
}
Feedback(on: VolumeState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
if state.value <= targetedVolume {
return Empty().eraseToAnyPublisher()
}
return Just(DecreaseEvent()).eraseToAnyPublisher()
}
}
Transitions {
From(VolumeState.self) { state in
On(IncreaseEvent.self, transitionTo: VolumeState(value: state.value + 1))
On(DecreaseEvent.self, transitionTo: VolumeState(value: state.value - 1))
}
}
}.execute(on: DispatchQueue(label: "A background queue"))
在典型情况下,副作用包含异步操作(如网络调用)。 如果重复调用相同的副作用,而没有等待前一个副作用结束会发生什么? 这些操作是否堆叠? 当执行新操作时是否取消它们?
嗯,这取决于 😁。 每个将 State 作为参数的反馈构造函数也可以传递一个 ExecutionStrategy
副作用不太可能不需要依赖项来执行其工作。 按照设计,副作用是一个只能将状态作为输入的函数。 幸运的是,Feedbacks 提供了工厂函数来帮助在副作用中注入依赖项。
enum MySideEffects {
static func load(
networkService: NetworkService,
databaseService: DataBaseService,
state: LoadingState
) -> AnyPublisher<Event, Never> {
networkService
.fetch()
.map { databaseService.save($0) }
.map { LoadedEvent(result: $0) }
.eraseToAnyPublisher()
}
}
let myNetworkService = MyNetworkService()
let myDatabaseService = MyDatabaseService()
let loadingEffect = SideEffect.make(MySideEffects.load, arg1: myNetworkService, arg2: myDatabaseService)
let feedback = Feedback(on: LoadingState.self, strategy: .cancelOnNewState, perform: loadingEffect)
SideEffect.make()
工厂会将具有多个参数(最多 6 个,包括状态)的函数转换为具有 1 个参数(状态)的函数,条件是状态是最后一个。
一个系统依赖于三件事:
一旦这些东西连接在一起,它就会形成一个 States 流,我们可以订阅它来运行 System
system.stream.sink { _ in }.store(&subscriptions)
or
system.run() // the subscription will live as long as the system is kept in memory
System 形成一个循环,也称为反馈循环,其中状态会不断调整,直到达到一个稳定值
这是支持的修饰符的列表
修饰符 | 动作 | 可以应用于 |
---|---|---|
.disable(disabled:) |
只要 disabled 条件为真,目标就不会被执行 |
|
.execute(on:) |
目标将在调度程序上执行 |
|
.onStateReceived(perform:) |
每次给定一个新状态作为输入时,执行 perform 闭包 |
|
.onEventEmitted(perform:) |
每次发出新事件时,执行 perform 闭包 |
|
.attach(to:) |
请参阅“如何使系统通信”部分 |
|
.uiSystem(viewStateFactory:) |
请参阅“将 Feedbacks 与 SwiftUI 和 UIKit 一起使用”部分 |
|
由于每个修饰符都返回目标的更新实例,因此我们可以将它们链接起来。
Feedback(...)
.execute(on: ...)
.onStateReceived {
...
}
.onEventEmitted {
...
}
尽管建议在状态机中描述所有可能的转换,但仍然可以使用通配符来简化操作。
Transitions {
From(ErrorState.self) {
On(AnyEvent.self, transitionTo: LoadingState())
}
}
考虑到状态是 ErrorState,无论接收到什么事件,此转换都会产生 LoadingState。
Transitions {
From(AnyState.self) {
On(RefreshEvent.self, transitionTo: LoadingState())
}
}
每次接收到 RefreshEvent 时,无论之前的状态如何,此转换都会产生 LoadingState。
Feedback 是从副作用构建的。 副作用是一个将状态作为参数的函数。 有两种构建 Feedback 的方法
Feedback(on: AnyState.self, strategy: .continueOnNewState) { state in
...
.map { _ in MyEvent() }
.eraseToAnyPublisher()
}
无论生成的状态类型如何,此反馈都将执行副作用。 如果你希望每次生成新状态时都执行副作用,而不管 State 的类型如何,这将非常有用。
Feedback(on: LoadingState.self, strategy: .continueOnNewState) { state in
...
.map { _ in MyEvent() }
.eraseToAnyPublisher()
}
此反馈仅当状态类型为 LoadingState 时才会执行副作用。
系统越复杂,我们就越需要添加转换。 将它们拆分为逻辑单元是一个很好的做法
let transitions = Transitions {
From(LoadingState.self) { state in
On(DataIsLoaded.self, transitionTo: LoadedState.self) { event in
LoadedState(page: state.page, data: event.data)
}
On(LoadingHasFailed.self, transitionTo: ErrorState())
}
From(LoadedState.self) { state in
On(RefreshEvent.self, transitionTo: LoadingState.self) {
LoadingState(page: state.page)
}
}
}
甚至将它们外部化为属性
let loadingTransitions = From(LoadingState.self) { state in
On(DataIsLoaded.self, transitionTo: LoadedState.self) { event in
LoadedState(page: state.page, data: event.data)
}
On(LoadingHasFailed.self, transitionTo: ErrorState())
}
let loadedTransitions = From(LoadedState.self) { state in
On(RefreshEvent.self, transitionTo: LoadingState.self) {
LoadingState(page: state.page)
}
}
let transitions = Transitions {
loadingTransitions
loadedTransitions
}
为了方便测试你的转换,你可以导入 "FeedbacksTest" 库。 它在 "Transitions" 类型上提供辅助函数。
一旦你有一个系统,你可以检索它的转换:let transitions = mySystem.transitions
transitions.assertThat(from: VolumeState(value: 10), on: IncreaseEvent(), newStateIs: VolumeState(value: 11))
transitions.assertThatStateIsUnchanged(from: Loading(), on: Refresh())
系统应该是自包含的,并且仅限于它们的业务。 我们应该注意使它们变得小巧且可组合。 可能出现一个功能由多个系统组成的情况。 在这种情况下,我们可能希望它们一起通信。
OOP 中有一种模式可以解决这个问题:中介者 (Mediator)。 中介者充当独立组件之间的通信总线,以保证它们的解耦。
Feedbacks 带有两种类型的中介者:CurrentValueMediator
和 PassthroughMediator
。 它们基本上是 CurrentValueSubject
和 PassthroughSubject
的类型别名。
将两个系统连接在一起
let mediator = PassthroughMediator<Int>()
let systemA = System {
...
}.attach(to: mediator, onSystemState: LoadedState.self, emitMediatorValue: { _ in 1701 })
let systemB = System {
...
}.attach(to: mediator, onMediatorValue: 1701 , emitSystemEvent: { _ in LoadedDoneEvent() }))
当 systemA 发出 LoadedState
状态时,中介者将在其订阅者之间传播 1701
值,并且 systemB 将触发 LoadedDoneEvent
。
当你没有同时引用这 2 个系统时,这种方法很好。 你可以传递中介者,或确保将公共实例注入到你,以建立系统之间的链接。
如果你偶然同时引用了这两个系统,则可以在没有中介者的情况下连接它们
let systemA = System {
...
}
let systemB = System {
...
}
systemA.attach(
to: systemB,
onSystemStateType: LoadedState.self,
emitAttachedSystemEvent: { stateFromA in
LoadedEvent(data: stateFromA.data)
}
)
当 systemA 遇到状态 LoadedState
时,systemB 将触发 LoadedEvent
事件。
尽管系统可以在没有视图的情况下独立存在,但在我们的开发人员世界中,将其视为一种生成将在屏幕上呈现的状态并期望用户发出的事件是有意义的。
幸运的是,将 State 作为渲染的输入并从用户交互返回事件流非常类似于副作用的定义; 我们知道如何处理它们 😁 -- 当然是用一个系统。 Feedbacks 提供了一个 UISystem
类,它是传统 System
的一个装饰器,但专门用于 UI 交互。
根据你的用例的复杂性,你可以使用两种方式使用 UISystem
System
实例化一个 UISystem
:生成的系统将发布一个 RawState
,它是状态的基本封装。 你必须在你的视图中编写函数来提取你需要的信息。 你可以在 CounterApp 演示应用程序 中找到实现的示例。System
和 viewStateFactory
函数实例化一个 UISystem
:生成的系统将发布一个 ViewState
,它是 viewStateFactory
函数的输出。 它允许实现更复杂的映射。 你可以在 GiphyApp 演示应用程序 中找到实现的示例。UISystem
有一些特点
state
属性,我们可以在 SwiftUI 视图或 UIKit ViewControllers 中监听emit(event:)
函数来在系统中传播用户事件enum FeatureViewState: State {
case .displayLoading
case .displayData(data: Data)
}
let stateToViewState: (State) -> FeatureViewState = { state in
switch (state) {
case is LoadingState: return .displayLoading
case let loadedState as LoadedState: return .displayData(loadedState.data)
...
}
}
let system = UISystem(viewStateFactory: stateToViewState) {
InitialState {
LoadingState()
}
Feedbacks {
...
}
Transitions {
...
}
}
或者,我们可以从传统 System
构建一个 UISystem
let system = System {
InitialState {
LoadingState()
}
Feedbacks {
...
}
Transitions {
...
}
}
let uiSystem = system.uiSystem(viewStateFactory: stateToViewState)
一旦启动,我们可以将 uiSystem
注入到 SwiftUI View 中
struct FeatureView: View {
@ObservedObject var system: UISystem<FeatureViewState>
var body: some View {
switch (self.system.state) {
case .displayLoading: ...
case let .displayData(data): ...
}
}
var button: some View {
Button {
Text("Click")
} label: {
self.system.emit(RefreshEvent())
}
}
}
或者注入到 ViewController 中
class FeatureViewController: ViewController {
var subscriptions = [AnyCancellable]()
func viewDidLoad() {
self
.system
.$state
.sink { [weak self] state in self?.render(state) }
.store(in: &self.subscriptions)
}
func onClick() {
self.system.emit(RefreshEvent())
}
}
你将在项目的 Examples 文件夹中找到一个演示应用程序。 随着时间的推移,我们将添加新的示例。