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.
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.