Swift Signal 是一个 Swift 包,它提供了受 Solid 启发的响应式计算功能。
如果您熟悉 Solid 或信号 (signals),那么将很容易上手此包。API 设计与 Solid 大致相同,除了语言风格上的差异。
该包分为两个库:核心库和 SwiftUI 集成库。
警告
这是一个实验性包。如果您正在开发生产应用程序,请谨慎使用。
在您的 Package.swift
清单文件中,将以下依赖项添加到您的 dependencies 参数中
.package(url: "https://github.com/unixzii/swift-signal.git", branch: "main"),
将依赖项添加到您在清单文件中声明的任何目标 (targets) 中
.target(
name: "MyTarget",
dependencies: [
.product(name: "SwiftSignal", package: "swift-signal"),
// Also add this if you need SwiftUI integration.
.product(name: "SwiftUISignal", package: "swift-signal"),
]
),
信号 (Signals) 是最基本的响应式原语。它们跟踪随时间变化的单个值。要创建信号,只需实例化一个 Signal
对象
let count = Signal(initialValue: 1)
let ready = Signal(initialValue: true)
您可以调用信号作为函数来读取其当前值
print(count())
要设置或更新信号的值,请分别调用 set
或 update
方法
count.set(42)
count.update { $0 + 1 }
Effect 是一种通用方法,用于在依赖项发生更改时运行任意代码(“副作用”)。Effect 创建一个计算,该计算在跟踪作用域中运行给定的闭包,从而自动跟踪其依赖项,并在依赖项更新时自动重新运行闭包。要创建 effect,请调用 createEffect
函数
let count = Signal(initialValue: 0)
let disposeEffect = createEffect {
print(count())
return nil
}
count.set(1)
运行代码将收到以下输出
0
1
要处置(销毁)effect,只需调用 createEffect
返回的闭包即可。
注意
您必须保留处置闭包的引用,否则 effect 可能无法按预期工作。
您可以在 createEffect
内部返回一个清理闭包,该闭包将在处置时或每次 effect 的依赖项更改时调用
createEffect {
return {
print("cleanup code here")
}
}
createComputed
通过执行给定的闭包来创建 computed(派生)值。它返回一个 getter 闭包来检索计算值。计算闭包仅在其依赖项更改时执行。
let signalA = Signal(initialValue: 0)
let signalB = Signal(initialValue: 0)
let sum = createComputed {
return signalA() + signalB()
}
此原语类似于 Solid 中的 createMemo
。您可以使用 createComputed
包装耗时的计算以优化性能。通常,记住将多次执行的计算是一个好习惯。
注意
与 createEffect
不同,computed 闭包仅在显式读取或被 effect 观察时才会执行。如果您想对信号或 computed 值的更改做出反应,请使用 createEffect
。
通过导入 SwiftUISignal
模块,您可以将信号与 SwiftUI 集成。我们将通过一个简单的应用程序来演示它。
首先,创建一些响应式值
let counterA = Signal(initialValue: 1)
let counterB = Signal(initialValue: 1)
let selectedCounter = Signal(initialValue: 1)
let message = createComputed {
if selectedCounter() == 1 {
return "Counter A: \(counterA())"
} else {
return "Counter B: \(counterB())"
}
}
let sum = createComputed {
return counterA() + counterB()
}
然后,您可以使用 ObservedComputed
从 SwiftUI 视图中读取它
struct MessageView: View {
@ObservedObject private var observedMessage = ObservedComputed {
return message()
}
var body: some View {
Text(observedMessage())
}
}
struct SumView: View {
@ObservedObject private var observedSum = ObservedComputed {
return "Sum: \(sum())"
}
var body: some View {
Text(observedSum())
}
}
每次依赖项更改时,依赖视图将自动更新。
最后,在根视图中组合它们
struct ContentView: View {
var body: some View {
VStack {
MessageView()
SumView()
Toggle("Selected Counter", isOn: .init(get: {
return selectedCounter() == 2
}, set: { newValue in
selectedCounter.write(newValue ? 2 : 1)
}))
.toggleStyle(SwitchToggleStyle())
HStack {
Button("+A") {
counterA.update { $0 + 1 }
}
Button("+B") {
counterB.update { $0 + 1 }
}
}
}
.padding()
}
}
您可以试用该应用程序,并通过观察每个视图的更新来探索细粒度的响应性。
ObservedSignal
和 ObservedComputed
都符合 ObservableObject
协议,该协议必须与 @StateObject
或 @ObservedObject
属性包装器一起使用。以您处理其他普通 ObservableObject
模型的方式做出决定。通常,@StateObject
用于提供单一数据源,而 @ObservedObject
用于观察传入的属性。
Signal 是在 SwiftUI 和其他 UI 框架之间传递数据的更好方法。例如,您可以在 AppKit 视图控制器中创建一个信号,并将其传递给托管的 SwiftUI 视图。然后,您可以方便地在 SwiftUI 环境之外更新 SwiftUI 视图,并使用 effect 创建双向绑定。
class ViewController: NSViewController {
let count = Signal(initialValue: 0)
var countEffect: DisposeAction?
override func loadView() {
view = NSHostingView(rootView: MyView(count: count))
countEffect = createEffect { [unowned self] in
let currentCount = count()
// Handle count changes...
return nil
}
}
}
struct MyView: View {
@ObservedObject private var count: ObservedComputed<Int>
private let countSignal: Signal<Int>
init(count: Signal<Int>) {
self.count = .init {
return count()
}
self.countSignal = count
}
var body: some View {
VStack {
Text("\(count())")
Button("Increase") {
countSignal.update { $0 + 1 }
}
}
}
}
欢迎提交 Pull Request。在这个阶段,我们仍在评估信号在 Swift 中的可能性。在进行重大更改之前,请先提出 issue。
根据 MIT 许可证获得许可,有关更多信息,请参阅 LICENSE。