Chapters

Hide chapters

Combine: Asynchronous Programming with Swift

Third Edition · iOS 15 · Swift 5.5 · Xcode 13

12. Key-Value Observing
Written by Florent Pillet

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 an object‘s property 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 a KVO-compliant property, 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: Apple does not provide a central list of KVO-compliant properties throughout its 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, or even in the system headers themselves.

Preparing and subscribing to your own KVO-compliant properties

You can also use Key-Value Observing in your own code, provided that:

  • Your objects are classes (not structs) and conform to NSObject,
  • You mark the properties to make observable with the @objc dynamic attributes.

Once you have done this, the objects and properties you marked become KVO-compliant and can be observed with Combine!

Note: While the Swift language doesn’t directly support KVO, marking your properties @objc dynamic forces the compiler to generate hidden methods that trigger the KVO machinery. Describing this machinery is out of the scope of this book. Suffice to say the machinery heavily relies on specific methods from the NSObject protocol, which explains why your objects need to conform to it.

Try an example in a 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

In the above code, you:

  1. Create a class that conforms to the NSObject protocol. This is required for KVO.
  2. Mark any property you want to make observable as @objc dynamic.
  3. Create and subscribe to a publisher observing the integerProperty property of obj.
  4. Update the property a couple times.

When running this code in a playground, can you guess what the debug console displays?

You may be surprised, but here is the display you obtain:

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

You first get the initial value of integerProperty, which is 0, then you receive the two changes. You can avoid this initial value if you‘re not interested in it — read on to find out how!

Did you notice that in TestObject you are using a plain Swift type (Int) and that KVO, which is an Objective-C feature, still works? KVO will work fine with any Objective-C type and with any Swift type bridged to Objective-C. This includes all the native Swift types as well as arrays and dictionaries, provided their values are all bridgeable to Objective-C.

Try it! Add a couple more properties to TestObject:

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

As well as subscriptions to their publishers:

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

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

And finally, some property changes:

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

You‘ll see both initial values and changes appear in your debug area. Nice!

If you ever use a pure-Swift type that isn‘t bridged to Objective-C though, you‘ll start running into trouble:

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

Then, add a property to TestObject:

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

You‘ll immediately see an error in Xcode, stating that “Property cannot be marked @objc because its type cannot be represented in Objective-C.” Here, you reached the limits of Key-Value Observing.

Note: Be careful when observing changes to system frameworks objects. Make sure the documentation mentions the property is observable because you can‘t have a clue by just looking at a system object‘s property list. This is true for Foundation, UIKit, AppKit, etc. Historically, properties had to be made “KVO-aware” to be observable.

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:

  • .initial emits the initial value.
  • .prior emits both the previous and the new value when a change occurs.
  • .old and .new are unused in this publisher, they both do nothing (just let the new value through).

If you don‘t want the initial value, you can simply write:

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

If you specify .prior, you‘ll get two separate values every time a change occurs. Modifying the integerProperty example:

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

You would now see the following in the debug console for the integerProperty subscription:

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

The property first changes from 0 to 100, so you get two values: 0 and 100. Then, it changes from 100 to 200 so you again get two values: 100 and 200.

ObservableObject

Combine‘s ObservableObject protocol works on Swift objects, not just on objects deriving from NSObject. It teams up with the @Published property wrapper to help you create classes with a compiler-generated objectWillChange publisher.

It saves you from writing a lot of boilerplate and allows creating objects which self-monitor their own properties and notify when any of them will change.

Here is an example:

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"

The ObservableObject protocol conformance makes the compiler automatically generate the objectWillChange property. It‘s an ObservableObjectPublisher which emits Void items and Never fails.

You‘ll get objectWillChange firing every time one of the object‘s @Published variables change. Unfortunately, you can‘t know which property actually changed. This is designed to work very well with SwiftUI which coalesces events to streamline screen updates.

Key points

  • Key-Value Observing mostly relies on the Objective-C runtime and methods of the NSObject protocol.
  • Many Objective-C classes in Apple frameworks offer some KVO-compliant properties.
  • You can make your own properties observable, provided they are classes conforming to NSObject, and marked with the @objc dynamic attributes.
  • You can also conform to ObservableObject and use @Published for your properties. The compiler-generated objectWillChange publisher triggers every time one of the @Published properties changes (but doesn’t tell you which one changed).

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!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.