Observable(可观察对象)

一组精心准备的类和协议,旨在为继承您的对象类型注入高效、协议驱动的观察者模式行为。从 2.0.0 版本开始,它包括对键控观察者的支持(有关详细信息,请参见下面的用法示例)

安装

Xcode 项目

选择 File -> Swift Packages -> Add Package Dependency 并输入 https://github.com/Flowduino/Observable.git

Swift Package Manager 项目

您可以在您自己的 Package 的 Package.swift 文件中使用 Observable 作为 Package 依赖项

let package = Package(
    //...
    dependencies: [
        .package(
            url: "https://github.com/Flowduino/Observable.git",
            .upToNextMajor(from: "2.0.0")
        ),
    ],
    //...
)

从那里,在任何需要它的您的 package 的 targets 中将 Observable 称为“target dependency”。

targets: [
    .target(
        name: "YourLibrary",
        dependencies: [
          "Observable",
        ],
        //...
    ),
    //...
]

然后您可以在任何需要它的代码中执行 import Observable

用法

以下是 Observable 提供的功能的一些快速简便的用法示例

ObservableClass

您可以在自己的类类型中继承 ObservableClass 以提供开箱即用的观察者模式支持。这不仅适用于 SwiftUI View 中带有 @ObservedObject 修饰的变量,而且还适用于您的类之间(例如,服务之间或存储库之间等)。

首先,您需要定义一个协议,描述您的观察者类类型中实现的方法,您的可观察对象类可以调用这些方法。

/// Protocol defining what Methods the Obverable Class can invoke on any Observer
protocol DummyObservable: AnyObject { // It's extremely important that this Protocol be constrained to AnyObject
    func onFooChanged(oldValue: String, newValue: String)
    func onBarChanged(oldValue: String, newValue: String)
}

注意 - 重要的是我们的协议定义了 AnyObject 一致性约束,如上所示。

现在,我们可以定义我们的可观察对象,它继承自 ObservableClass

/// Class that can be Observed 
class Dummy: ObservableClass {
    private var _foo: String = "Hello"
    public var foo: String {
        get {
            return _foo
        }
        set {
            // Invoke onFooChanged for all current Observers
            withObservers { (observer: DummyObservable) in
                observer.onFooChanged(oldValue: _foo, newValue: newValue)
            }
            _foo = newValue
            objectWillChange.send() // This is for the standard ObservableObject behaviour (both are supported together)
        }
    }

    private var _bar: String = "World"
    public var bar: String {
        get {
            return _bar
        }
        set {
            // Invoke onBarChanged for all current Observers
            withObservers { (observer: DummyObservable) in
                observer.onBarChanged(oldValue: _bar, newValue: newValue)
            }
            _bar = newValue
            objectWillChange.send() // This is for the standard ObservableObject behaviour (both are supported together)
        }
    }
}

我们现在可以定义一个观察者来向可观察对象注册,确保我们指定它实现了我们的 DummyObservable 协议

class DummyObserver: DummyObservable {
    /* Implementations for DummyObservable */
    func onFooChanged(oldValue: String, newValue: String) {
        print("Foo Changed from \(oldValue) to \(newValue)")
    }

    func onBarChanged(oldValue: String, newValue: String) {
        print("Bar Changed from \(oldValue) to \(newValue)")
    }
}

我们现在可以生成一些简单的代码(例如在 Playground 中)将它们组合在一起

// Playground Code to use the above
var observable = Dummy() // This is the Object that we can Observe
var observer = DummyObserver() // This is an Object that will Observe the Observable

observable.addObserver(observer) // This is what registers the Observer with the Observable!
observable.foo = "Test 1"
observable.bar = "Test 2"

ObservableThreadSafeClass

ObservableThreadSafeClass 的工作方式与 ObservableClass 完全相同。内部实现只是将 Observer 集合封装在 DispatchSemaphore 之后,并提供一个旋转门机制,以确保即使在执行 withObservers 时,也可以不受阻碍地访问 addObserverremoveObserver

它的用法与上面在 ObservableClass 中显示的完全相同,只是您将继承 ObservableClass 替换为继承自 ObservableThreadSafeClass

ObservableThread

ObservableThread 为您提供了一个基本类型,用于您想要观察的任何线程类型。

注意 - ObservableThread 确实实现了 ObservableObject 协议,并且技术上与 SwiftUI View 中的 @ObservedObject 属性装饰器兼容。但是,要以这种方式使用它,在任何您将调用 objectWillUpdate.send() 的地方,您都必须改为使用 notifyChange()。在内部,ObservableThread 将执行 objectWillChange.send()强制它必须在 MainActor 上执行(正如 Swift 所要求的)

现在让我们开始看看如何在您的代码中使用 ObservableThread。该示例是有意简化的,只是在线程中的无限循环中每 60 秒生成一个随机数。

让我们首先定义我们的观察协议

protocol RandomNumberObserver: AnyObject {
    func onRandomNumber(_ randomNumber: Int)
}

我们线程的任何观察者都需要符合上面的 RandomNumberObserver 协议。

现在,让我们定义我们的 RandomNumberObservableThread 类

class RandomNumberObservableThread: ObservableThread {
    init() {
        self.start() // This will start the thread on creation. You aren't required to do it this way, I'm just choosing to!
    }

    public override func main() { // We must override this method
        while self.isExecuting { // This creates a loop that will continue for as long as the Thread is running!
            let randomNumber = Int.random(in: -9000..<9001) // We'll generate a random number between -9000 and +9000
            // Now let's notify all of our Observers!
            withObservers { (observer: RandomNumberObserver) in
                observer.onRandomNumber(randomNumber)
            }
            Self.sleep(forTimeInterval: 60.00) // This will cause our Thread to sleep for 60 seconds
        }
    }
}

所以,我们现在有一个可以被观察的线程,并且每当它生成一个随机整数时,每分钟都会通知所有观察者。

现在让我们实现一个旨在观察这个线程的类

class RandomNumberObserverClass: RandomNumberObserver {
    public func onRandomNumber(_ randomNumber: Int) {
        print("Random Number is: \(randomNumber)")
}

我们现在可以在一个简单的 Playground 中将它们组合在一起

var myThread = RandomNumberObservableThread()
var myObserver = RandomNumberObserverClass()
myThread.addObserver(myObserver)

就这样! Playground 程序现在只需每 60 秒将新的随机数通知消息打印到控制台输出中。

您可以为所需的任何基于观察的线程行为采用这种方法,因为 ObservableThread 将始终在其自己的线程的执行上下文中调用观察者回调方法!这意味着,例如,您可以安全地在 UI 线程上实例化一个观察者类,而正在观察的代码执行驻留在其自己的线程中(每个需求一个或多个)。

键控观察模式

从 1.1.0 版本开始,您现在可以注册和通知键控观察者注意:2.0.0 版本对界面进行了重大修改,消除了对 Key 进行泛型类型化的需求。现在可以为您推断 Key 类型。

此功能是标准观察者模式的扩展,并在您可以扩展的以下类中实现

请记住,键控观察是基本观察模式的扩展,因此任何键控可观察对象本质上都能够注册和通知非键控观察者

每当您的观察者关心特定的更改上下文时,您就会使用键控观察。一个很好的例子是模型存储库,其中观察者可能只关心对存储库中包含的特定模型的更改。在这种情况下,您将使用键控观察来确保仅通知观察者与给定 Key 对应的更改。

Key 类型必须始终符合 Hashable 协议,就像用于 Dictionary 集合的任何 Key 类型一样。

让我们看一个基本用法示例。

我们将提供一个基本用法示例,仅使用来自 Observable 的内部 Dictionary 的值同步观察者的内部 Dictionary 以获取特定键。

首先,我们将从观察协议开始

protocol TestKeyedObservable: AnyObject {
    func onValueChanged(key: String, oldValue: String, newValue: String)
}

上面的观察协议提供了方法 onValueChanged,它采用 key(在本例中为 String 值),并提供该 key 的相应 oldValuenewValue 值。我们的观察者将实现 TestKeyedObservable 以提供此函数的实现。

现在,让我们定义一个简单的键控可观察对象,以容纳我们将有选择地与一个或多个观察者同步的主 Dictionary。

class TestKeyedObservableClass: KeyedObservableClass<String> {
    private var keyValues: [String:String] = ["A":"Hello", "B":"Foo", "C":"Ping"]
    
    func setValue(key: String, value: String) {
        withKeyedObservers(key: key) { (key, observer: TestKeyedObservable) in
            observer.onValueChanged(key: key, oldValue: self.keyValues[key]!, newValue: value)
        }
        self.keyValues[key] = value
    }
}

上面的类继承自 KeyedObservableClass 并专门化 TKey 泛型为 String。换句话说,此 Observable 的 Key 必须始终是 String 值。它包括一个简单的 String:String 字典(String 键和 String 值)

setValue 方法将简单地使用 withKeyedObservers 通知所有观察者,任何时候观察者正在观察的特定 key 被更新,传递 oldValuenewValue 值。然后它将更新其内部 Dictionary (keyValues),以便它始终包含最新值。

请注意使用 withKeyedObservers 而不是 withObservers。您将在您自己的键控可观察对象中使用此语法,仅更改声明的观察者协议(在本例中为 TestKeyedObservable)以及表示您自己的观察方法的观察者协议。

现在我们有一个键控可观察对象,它将在每次键的值更改时通知观察者,让我们定义一个观察者。

class TestKeyedObserverClass: TestKeyedObservable {
    public var keyValues: [String:String] = ["A":"Hello", "B":"Foo"]
    
    func onValueChanged(key: String, oldValue: String, newValue: String) {
        keyValues[key] = newValue
    }
}

因此,TestKeyedObserverClass 是一个简单的类,实现了我们的 TestKeyedObservable 观察者协议。对于此示例,我们假设有 2 个具有已知初始值的预定义 Key(不必如此...您可以拥有任意数量的 Key)

您会注意到我们初始化了 Observable 和 Observer 类以具有相同的 keyValues 字典。这仅仅是为了简化此示例,方法是确保始终存在 oldValue。您无需在自己的实现中执行此操作。

所以,既然我们有了可观察对象观察者类型,让我们生成一段简单的 Playground 代码将它们组合在一起。

let observable = TestKeyedObservableClass() // Creates our Observable
let observer = TestKeyedObserverClass // Creates a single Observer instance

此时,我们需要考虑我们的 observer 将要观察哪个或哪些 Key。

例如,我们可以仅观察一个 key

observable.addKeyedObserver(key: "A", observer)

上面的意思是只有当 observable 中修改了 key A 的值时,才会调用 observeronValueChanged 方法。

同样,如果我们只关心 key B,我们可以这样做

observable.addKeyedObserver(key: "B", observer)

如果我们关心两个已知 key,我们可以简单地将它们都注册

observable.addKeyedObserver(keys: ["A", "B"], observer)

此外,我们可以做一些特别聪明的事情,并且基本上为自己的 Dictionary 已知的每个 Key 注册 Observer

observable.addKeyedObserver(keys: Array(observer.keyValues.keys), observer)

上面将为 observerkeyValues 字典中包含的每个 keyobservable 注册 observer

最终,您可以为所需的任何 key 向 observable 注册 observer

observable.addKeyedObserver(key: "Foo", observer)

让我们在调用任何会修改其值的代码之前输出我们所有 key 的初始值

for (key, value) in observer.keyValues {
    print("Key: '\(key)' has a value of '\(value)'")
}

这将输出

Key: 'A' 的值为 'Hello' Key: 'B' 的值为 'Foo'

所以,既然我们可以为我们想要的任何 key 向 Observer 注册键控观察者,让我们在 observer 中触发观察者模式

observable.setValue(key: "A", "World")

上面会将 A 的值从“Hello”更新为“World”。

如果我们重复以下代码

for (key, value) in observer.keyValues {
    print("Key: '\(key)' has a value of '\(value)'")
}

这将输出

Key: 'A' 的值为 'World' Key: 'B' 的值为 'Foo'

好的,如果我们更改 key "C" 的值会怎样?会发生什么?

observable.setValue(key: "C", "Pong")

现在,如果我们重复以下代码

for (key, value) in observer.keyValues {
    print("Key: '\(key)' has a value of '\(value)'")
}

这将输出

Key: 'A' 的值为 'World' Key: 'B' 的值为 'Foo'

请注意,没有通知 observer 更改 key C 的值。这是因为 observer 没有观察 observable 是否更改 key C

这就是键控观察模式的价值。简而言之:并非所有观察对所有观察者都有意义。因此,正如您现在所看到的,键控观察使我们的观察者能够专门收到与该观察者相关的更改的通知。

重载的 addObserverremoveObserveraddKeyedObserverremoveKeyedObserver 方法

从 1.1.0 版本开始,已为 ObservableClassObservableThreadObservableThreadSafeClassKeyedObservableClassKeyedObservableThreadKeyedObservableThreadSafeClass 的上述指定方法提供所有有用的组合重载,以简化添加和删除带有/来自可观察对象观察者

将单个观察者添加到可观察对象

observable.addObserver(myObserver)

将多个观察者添加到可观察对象

observable.addObserver([myObserver1, myObserver2, myObserver3])

使用单个 Key 将单个键控观察者添加到键控可观察对象

keyedObservable.addKeyedObserver("MyKey", myKeyedObserver)

使用多个 Key 将单个键控观察者添加到键控可观察对象

keyedObservable.addKeyedObserver(["Key1", "Key2", "Key3"], myKeyedObserver)

使用单个 Key 将多个键控观察者添加到键控可观察对象

keyedObservable.addKeyedObserver("MyKey", [myKeyedObserver1, myKeyedObserver2, myKeyedObserver3])

使用多个 Key 将多个键控观察者添加到键控可观察对象

keyedObservable.addKeyedObserver(["Key1", "Key2", "Key3"], [myKeyedObserver1, myKeyedObserver2, myKeyedObserver3])

removeObserverremoveKeyedObserver 也提供与上面所示相同的重载。

其他有用的提示

关于此软件包,您应该了解以下一些有用的补充信息。

一个 Observable 可以为任意数量的 Observer Protocol 调用 withObservers

此库有意对每个注册的 Observer 执行运行时类型检查,以确保它符合您的 withObservers Closure 方法所明确定义的 Observer Protocol

简单的示例协议

protocol ObserverProtocolA: AnyObject {
    func doSomethingForProtocolA()
}

protocol ObserverProtocolB: AnyObject {
    func doSomethingForProtocolB()
}

然后,这两个协议可以被同一个 ObservableClassObservableThreadSafeClassObservableThread 后代类使用。

withObservers { (observer: ObserverProtocolA) in
    observer.doSomethingForProtocolA()
}

withObservers { (observer: ObserverProtocolB) in
    observer.doSomethingForProtocolB()
}

任意数量的 Observer Protocol 可以由我们的任何 Observable 类型进行编组,并且只有符合明确指定的 Observer ProtocolObserver 会被传递到您的 withObservers Closure 方法中。

许可证

Observable 在 MIT 许可证下可用。 有关更多信息,请参见 LICENSE 文件

加入我们的 Discord

如果您需要更多支持,或者想讨论 Observable、Swift 或任何其他与 Flowduino 相关的主题,可以加入我们的 Discord