12
Key-Value Observing
Written by Florent Pillet
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
Dealing with change is at the core of Combine. Publishers let you subscribe to them to handle asynchronous events. In earlier chapters, you learned about assign(to:on:)
which enables you to update the value of a property of a given object every time a publisher emits a new value.
But, what about a mechanism to observe changes to single variables?
Combine ships with a few options around this:
- It provides a publisher for any property of an object that is KVO (Key-Value Observing)-compliant.
- The
ObservableObject
protocol handles cases where multiple variables could change.
Introducing publisher(for:options:)
KVO has always been an essential component of Objective-C. A large number of properties from Foundation, UIKit and AppKit classes are KVO-compliant. Therefore, you can observe their changes using the KVO machinery.
It’s easy to observe KVO-compliant properties. Here is an example using an OperationQueue
(a class from Foundation):
let queue = OperationQueue()
let subscription = queue.publisher(for: \.operationCount)
.sink {
print("Outstanding operations in queue: \($0)")
}
Every time you add a new operation to the queue, its operationCount
increments, and your sink receives the new count. When the queue has consumed an operation, the count decrements and again, your sink receives the updated count.
There are many other framework classes exposing KVO-compliant properties. Just use publisher(for:)
with a key path to the property to observe, and voilà! You get a publisher capable of emitting value changes. You’ll learn more about this and available options later in this chapter.
Note: There is no central list of KVO-compliant properties throughout the frameworks. The documentation for each class usually indicates which properties are KVO-compliant. But sometimes the documentation can be sparse, and you’ll only find a quick note in the documentation for some of the properties.
Preparing and subscribing to your own KVO-compliant properties
You can also use Key-Value Observing in your own code, provided that:
// 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
integerProperty changes to 0
integerProperty changes to 100
integerProperty changes to 200
@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]
struct PureSwift {
let a: (Int, Bool)
}
@objc dynamic var structProperty: PureSwift = .init(a: (0,false))
Observation options
The full signature of the method you are calling to observe changes is publisher(for:options:)
. The options
parameter is an option set with four values: .initial
, .prior
, .old
and .new
. The default is [.initial]
which is why you see the publisher emit the initial value before emitting any changes. Here is a breakdown of the options:
obj.publisher(for: \.stringProperty, options: [])
let subscription = obj.publisher(for: \.integerProperty, options: [.prior])
integerProperty changes to 0
integerProperty changes to 100
integerProperty changes to 100
integerProperty changes to 200
ObservableObject
Combine‘s ObservableObject
protocol works on Swift objects, not just on objects derived from NSObject
. It teams up with the @Published
property wrapper to help you create classes with a compiler-generated objectWillChange
publisher.
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"
Key points
Where to go from here?
Observing is a lot of fun, but sharing is caring! Keep reading to learn about Resources in Combine, and how you can save them by sharing them!