Key-Value Observing (KVO) and Swift 3

Key-Value Observing, KVO for short, is an important concept of the Cocoa API. It allows objects to be notified when the state of another object changes. That sounds very useful. Right?

Despite the promise Key-Value Observing holds, it is an API very few developers enjoy using. Key-Value Observing itself is great, its API not so much. There is an upside, though. The API is very concise. Therefore, it is easy to get up to speed with KVO.

Let's Keep It Informal

Powering Key-Value Observing is the NSKeyValueObserving protocol. The documentation states that NSKeyValueObserving is an informal protocol. The NSObject root class conforms to the NSKeyValueObserving protocol and any class that inherits from NSObject is also assumed to conform to the protocol.

Later in this tutorial, we find out what that means for developers. For now, remember that every class that is defined in the Foundation framework and that inherits from NSObject conforms to the NSKeyValueObserving protocol.

What's the deal with the UIKit framework? That is a great question. Apple is a bit vague about the implementation of KVO in UIKit. This is what the documentation has to say about KVO and UIKit.

Although the classes of the UIKit framework generally do not support KVO, you can still implement it in the custom objects of your application, including custom views. — Cocoa Core Competencies

What does generally do not support mean? It means that some classes support KVO while other don't. You need to consult the documentation to find out whether the class you are working with conforms to the NSKeyValueObserving protocol. Don't simply assume it does.

Project Setup

Let me show you how Key-Value Observing works with an example. Fire up Xcode and create a new project based on the Single View Application template. Create two classes that inherit from NSObject:

  • Configuration
  • ConfigurationManager

The implementation of Configuration is trivial as you can see below.

import Foundation

class Configuration: NSObject {

    // MARK: - Properties

    var createdAt = Date()
    var updatedAt = Date()

}

The implementation of ConfigurationManager is also easy to understand.

import UIKit

class ConfigurationManager: NSObject {

    // MARK: - Properties

    var configuration: Configuration

    // MARK: -

    lazy private var dateFormatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
        return dateFormatter
    }()

    // MARK: -

    var createdAt: String {
        return dateFormatter.string(from: configuration.createdAt)
    }

    var updatedAt: String {
        return dateFormatter.string(from: configuration.updatedAt)
    }

    // MARK: - Initialization

    init(withConfiguration configuration: Configuration) {
        self.configuration = configuration

        super.init()
    }

    // MARK: - Public Interface

    func updateConfiguration() {
        configuration.updatedAt = Date()
    }

}

The configuration manager manages a Configuration instance. The Configuration class defines two properties, createdAt and updatedAt. Both are of type Date. Even though the example is a bit contrived, it is prefect for showing the ins and outs of KVO.

Open the ViewController class and define a property for the configuration manager and an outlet for a UILabel instance.

import UIKit

class ViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var timeLabel: UILabel!

    // MARK: -

    let configurationManager = ConfigurationManager(withConfiguration: Configuration())

    ...
}

Define an action, updateConfiguration(sender:), in which the configuration the configuration manager manages is updated.

// MARK: - Actions

@IBAction func updateConfiguration(sender: UIButton) {
    configurationManager.updateConfiguration()
}

The user interface of the ViewController class is very basic as you can see below. Don't forget to wire up the outlet and action we defined in the ViewController class.

User Interface of View Controller

If you run the application and tap the Update Configuration button, nothing happens. Even though the configuration's updatedAt property is updated, the value of the dateLabel property isn't. That is something we can solve with Key-Value Observing.

Adding an Observer

The underlying idea of Key-Value Observing is simple. When an object is added as an observer for a particular key path, it is notified when the property the object is observing changes. Even though the API isn't great, it has improved a little in Swift 3.

The goal is to update the value of the label whenever the value of the updatedAt property changes. This means the view controller needs to be notified of the change. In KVO parlance, we need to add the view controller as an observer. Update the viewDidLoad() method of the ViewController class as shown below.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    addObserver(self, forKeyPath: #keyPath(configurationManager.configuration.updatedAt), options: [.old, .new], context: nil)
}

In viewDidLoad(), the view controller adds itself as an observer for the updatedAt property of the configuration object. It does this by invoking addObserver(_:forKeyPath:options:context:), a method of the NSObject root class. Because the UIViewController class inherits from NSObject, this method is available to us.

The addObserver(_:forKeyPath:options:context:) method defines four parameters.

Observer

The first parameter is the object that is added as an observer. This can only be an instance of a class that inherits from the NSObject root class. It is the NSObject root class that defines the addObserver(_:forKeyPath:options:context:) method as well as the method that is invoked when a change is detected.

Key Path

The key path defines what property the observer is interested in. In the example, the view controller is added as an observer for the updatedAt property of the Configuration instance of the configuration manager of the view controller.

We use a #keyPath expression to define the key path. A key path is nothing more than a sequence of object properties. Before Swift 3, the key path was a string literal. Thanks to the addition of key path expressions, the compiler can check the validity of the key path at compile time. String literals don't have this advantage, which often lead to bugs in the past.

If the key path is deemed valid by the compiler, it is replaced by a string literal at compile time. Why? KVO uses the Objective-C runtime. In Objective-C, keys and key paths are represented by strings. And remember that KVO is only possible because Swift uses the Objective-C runtime.

Notice that the key path used in addObserver(_:forKeyPath:options:context:) is relative to the current scope and context.

Options

You can optionally pass in a list of options to addObserver(_:forKeyPath:options:context:). The default is an empty option set. The list of options determines what information the observer is given when a change of the property it observes occurs. But it also defines when the observer needs to be notified of changes.

  • new: This option ensures that the change dictionary includes the new value of the observed property.
  • old: This option ensures that the change dictionary includes the old value of the observed property.
  • initial: By including this option in the list of options, the observer is immediately sent a notification, before it is added as an observer.
  • prior: This is an option you rarely use. This option ensures the observer receives a notification before and after a change occurs.

Context

The context is a more advanced option you will rarely use. It allows you to pass additional data to the observer when a notification is sent.

Handling Notifications

The view controller is added as an observer. How can it respond to changes? Simple. The view controller overrides observeValue(forKeyPath:of:change:context:), another method defined by the NSObject root class. This method also defines four parameters.

Key Path

The first parameters is the key path that triggered the notification.

Object

The observer is also given a reference to the object it is observing.

Changes

The observer receives a dictionary of type [NSKeyValueChangeKey : Any]?. This dictionary can contain a number of key-value pairs. The contents depend on the options passed to addObserver(_:forKeyPath:options:context:).

Context

This is the context that was passed in when the observer was added earlier.

One of the most important downsides of KVO is that every notification needs to be handled in the observeValue(forKeyPath:of:change:context:) method. This can get messy pretty quickly. The example we are working with is simple, though.

// MARK: - Key-Value Observing

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == #keyPath(configurationManager.configuration.updatedAt) {
        // Update Time Label
        timeLabel.text = configurationManager.updatedAt
    }
}

Even though the keyPath parameter is of type String?, we can use a #keyPath expression for comparison. This ensures the compiler does the necessary validation at compile time.

If we detect that the value of the updatedAt property of the configuration is modified, we update the value of the date label. We could pull the value from the change dictionary. The problem is that the values in the dictionary are of type Any. It is easier and safer to ask the configuration manager for the formatted value.

Run the application to see if the value of the date label is updated if you tap the Update Configuration button.

Objective-C and Dynamic Dispatch

What? It isn't working? Yesterday, I talked about the dynamic declaration modifier. In that tutorial, I also mentioned that Key-Value Observing is only possible thanks to the Objective-C runtime and dynamic dispatch in particular.

What is going wrong in this simple example? The compiler is smart enough to figure out how the updatedAt property of the Configuration class needs to be accessed. It doesn't need dynamic dispatch to figure this out at runtime. By bypassing dynamic dispatch it gains a few nanoseconds ... but it breaks KVO. Again, KVO relies on dynamic dispatch.

How can we fix this? Easy. We prefix the updatedAt property with the dynamic declaration modifier. This tells the compiler that the updatedAt property should always be accessed using dynamic dispatch.

import Foundation

class Configuration: NSObject {

    // MARK: - Properties

    dynamic var createdAt = Date()
    dynamic var updatedAt = Date()

}

Run the application again to see if this solves the issue. Note that I also marked the createdAt property as dynamic. This keeps the interface of the Configuration class consistent.

Displaying the Initial Value

When the application launches, the value of the date label is not correct. We want it to display the formatted value of the updatedAt property of the Configuration instance. Easy.

Do you remember the initial option of the addObserver(_:forKeyPath:options:context:) method we discussed earlier? Update the viewDidLoad() method as shown below and run the application again.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    addObserver(self, forKeyPath: #keyPath(configurationManager.configuration.updatedAt), options: [.old, .new, .initial], context: nil)
}

Even though the value of the updatedAt property of the Configuration instance hasn't changed yet, the observer does receive a notification. The result is that the view controller updates the value of the date label in observeValue(forKeyPath:of:change:context:).

Removing an Observer

Another major issue when working with Key-Value Observing is memory management. Observers need to be explicitly removed when they are no longer interested in receiving notifications for a particular key path. You have two options.

  • removeObserver(_:forKeyPath:)
  • removeObserver(_:forKeyPath:context:)

I assume that these methods don't need an explanation. It shows how clumsy the Key-Value Observing API is. You need to remove an observer for every key path it observes. If you forget to do this, you end up with a memory leak or, even worse, a crash.

// MARK: - Deinitialization

deinit {
    removeObserver(self, forKeyPath: #keyPath(configurationManager.configuration.updatedAt))
}

What's Next?

You should now have a good grasp of what Key-Value Observing is and how to use it in combination with Swift 3. Even though the API isn't great, the underlying concept is very powerful. It is a pattern that many other programming languages implement.

The problems of KVO have urged several developers at Facebook to come up with a better solution. KVOController is a library that makes working with KVO much easier and safer. It uses a modern API and guarantees thread safety. It is worth checking out.

You can download the source files from GitHub.

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By