Swift Signal

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"),
    ]
),

基本用法

Signal 类型

信号 (Signals) 是最基本的响应式原语。它们跟踪随时间变化的单个值。要创建信号,只需实例化一个 Signal 对象

let count = Signal(initialValue: 1)
let ready = Signal(initialValue: true)

您可以调用信号作为函数来读取其当前值

print(count())

要设置或更新信号的值,请分别调用 setupdate 方法

count.set(42)
count.update { $0 + 1 }

Effect (副作用)

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")
    }
}

Computed (派生) 值

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

在 SwiftUI 中使用

通过导入 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()
    }
}

您可以试用该应用程序,并通过观察每个视图的更新来探索细粒度的响应性。

@StateObject vs @ObservedObject

ObservedSignalObservedComputed 都符合 ObservableObject 协议,该协议必须与 @StateObject@ObservedObject 属性包装器一起使用。以您处理其他普通 ObservableObject 模型的方式做出决定。通常,@StateObject 用于提供单一数据源,而 @ObservedObject 用于观察传入的属性。

在 SwiftUI 和 AppKit / UIKit 之间传递数据

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