跳转至

第12章:键值观察

应对变化是Combine的核心。发布者允许您订阅它们来处理异步事件。在前面的章节中,您学习了assign(to:on:)它使您能够在发布者每次发布者发出新值时更新对象属性的值。

但是,观察单个变量变化的机制呢?

围绕以下几个选项组合船舶:

  • 它为符合KVO(键值观察)的对象的任何属性提供了一个发布者。
  • ObservableObject协议处理多个变量可能更改的情况。

介绍publisher(for:options:)

KVO一直是Objective-C的重要组成部分。FoundationUIKitAppKit类中的大量属性都符合KVO标准。因此,您可以使用KVO机器观察它们的变化。

很容易观察到符合KVO的属性。以下是使用OperationQueue(来自Foundation的类)的示例:

let queue = OperationQueue()

let subscription = queue.publisher(for: \.operationCount)
  .sink {
    print("Outstanding operations in queue: \($0)")
  }

每次您将新操作添加到队列中时,其operationCount都会增加,您的接收器都会收到新计数。当队列消耗了操作时,计数会减少,并且您的接收器再次收到更新的计数。

还有许多其他框架类暴露了符合KVO的属性。只需使用具有KVO合规属性的密钥路径的publisher(for:)voilà!您会得到一个能够发出价值变化的发布者。您将在本章后面了解有关此选项的更多信息以及可用选项。

Note

Apple不会在其整个框架中提供符合KVO的属性的中央列表。每个类的文档通常指示哪些属性符合KVO。但有时文档可能很稀疏,您只能在文档中找到一些属性的快速注释,甚至在系统头本身中找到。

准备和订阅您自己的符合KVO的属性

您还可以在自己的代码中使用键值观察,前提是:

  • 您的对象是类(不是结构),并且符合NSObject
  • 您标记属性,以便使用@objc dynamic属性进行可观察。

完成后,您标记的对象和属性将符合KVO,可以使用Combin进行观察!

Note

虽然Swift语言不直接支持KVO,但标记您的属性@objc dynamic迫使编译器生成触发KVO机器的隐藏方法。描述这台机器超出了本书的范围。只需说,该机器严重依赖NSObject协议中的特定方法,这解释了为什么您的对象需要符合它。

Playground尝试一个例子:

// 1
class TestObject: NSObject {
  // 2
  @objc dynamic var integerProperty: Int = 0
}

let obj = TestObject()

// 3
let subscription = obj.publisher(for: \.integerProperty)
  .sink {
    print("integerProperty changes to \($0)")
  }

// 4
obj.integerProperty = 100
obj.integerProperty = 200

在上述代码中,您:

  1. 创建一个符合NSObject协议的类。这是KVO所必需的。
  2. 将您想要可观察的任何属性标记为@objc dynamic
  3. 创建并订阅观察objintegerProperty属性的发布者。
  4. 更新几次属性。

Playground运行此代码时,您能猜出调试控制台显示的内容吗?

您可能会感到惊讶,但以下是您获得的显示器:

integerProperty changes to 0
integerProperty changes to 100
integerProperty changes to 200

您首先获得integerProperty的初始值,即0,然后收到两个更改。如果您对这个初始值不感兴趣,您可以避免它——继续阅读以了解如何!

您是否注意到,在TestObject中,您正在使用普通的Swift类型(Int),而Objective-C功能的KVO仍然有效?KVO适用于任何Objective-C类型和任何与Objective-C相连的Swift类型。这包括所有原生Swift类型以及数组和词典,前提是它们的值都可以桥接到Objective-C

试试看!向TestObject添加更多属性:

@objc dynamic var stringProperty: String = ""
@objc dynamic var arrayProperty: [Float] = []

以及对其发布者的订阅:

let subscription2 = obj.publisher(for: \.stringProperty)
  .sink {
    print("stringProperty changes to \($0)")
  }

let subscription3 = obj.publisher(for: \.arrayProperty)
  .sink {
    print("arrayProperty changes to \($0)")
  }

最后,一些财产变化:

obj.stringProperty = "Hello"
obj.arrayProperty = [1.0]
obj.stringProperty = "World"
obj.arrayProperty = [1.0, 2.0]

您会在调试区域中看到初始值和更改。很好!

不过,如果您使用没有与Objective-C相连的纯Swift类型,您将开始遇到麻烦:

struct PureSwift {
  let a: (Int, Bool)
}

然后,将属性添加到TestObject

@objc dynamic var structProperty: PureSwift = .init(a: (0,false))

您会立即在Xcode中看到一个错误,指出“属性无法标记为@objc,因为它的类型无法在Objective-C中表示。”在这里,您达到了键值观察的极限。

Note

观察系统框架对象的变化时要小心。确保文档提到属性是可观察的,因为您无法仅通过查看系统对象的属性列表来获取线索。FoundationUIKitAppKit 等都是如此。从历史上看,财产必须具有“KVO意识”才能被观察到。

观察选项

您调用的观察更改的方法的完整签名是publisher(for:options:) options参数是一个包含四个值的选项集:.initial.prior.old.new。默认值是[.initial],这就是为什么您看到发布者在发出任何更改之前发出初始值。以下是选项的细目:

  • .initial发出初始值。
  • .prior当发生更改时,同时发出以前的和新的值。
  • .old.new在这个发布者中没有使用,他们都什么都不做(只是让新价值通过)。

如果您不想要初始值,您可以简单地写以下内容:

obj.publisher(for: \.stringProperty, options: [])

如果您指定.prior,每次发生更改时,您将获得两个单独的值。修改integerProperty示例:

let subscription = obj.publisher(for: \.integerProperty, options: [.prior])

您现在会在integerProperty订阅的调试控制台中看到以下内容:

integerProperty changes to 0
integerProperty changes to 100
integerProperty changes to 100
integerProperty changes to 200

属性首先从0更改为100,因此您将获得两个值:0100。然后,它从100更改为200,因此您再次获得两个值:100200

可观测对象

CombineObservableObject协议适用于Swift对象,而不仅仅是来自NSObject的对象。它与@Published属性包装器合作,帮助您使用编译器生成的objectWillChange发布者创建类。

它使您免于编写大量样板,并允许创建自我监控自身属性并通知其任何属性何时更改的对象。

这里有一个例子:

class MonitorObject: ObservableObject {
  @Published var someProperty = false
  @Published var someOtherProperty = ""
}

let object = MonitorObject()
let subscription = object.objectWillChange.sink {
  print("object will change")
}

object.someProperty = true
object.someOtherProperty = "Hello world"

ObservableObject协议的一致性使编译器自动生成objectWillChange属性。这是一个ObservableObjectPublisher,它发出Void项,Never失败。

每当对象的@Published变量之一发生变化时,您都会收到objectWillChange触发。不幸的是,你不知道哪些房产实际上发生了变化。这旨在与SwiftUI配合得很好,SwiftUI合并事件以简化屏幕更新。

关键点

  • 键值观测主要依赖于Objective-C运行时和NSObject协议的方法。
  • Apple框架中的许多Objective-C类都提供了一些符合KVO的特性。
  • 您可以使自己的属性可观察,前提是它们是符合NSObject的类,并用@objc dynamic属性标记。
  • 您还可以遵守ObservableObject,并将@Published用于您的属性。编译器生成的objectWillChange发布者每次更改@Published属性之一时都会触发(但没有告诉您哪个属性更改)。

接下来去哪?

观察很有趣,但分享就是关怀!继续阅读以了解Combine中的资源,以及如何通过共享它们来保存它们!