Key-Value Observing (KVO) is a mechanism that allows objects in Swift (and Objective-C) to observe changes to properties of other objects. It’s particularly useful for responding to changes in model data, updating the UI, or handling other dependent computations automatically when data changes.

How KVO works

KVO is based on another mechanism called Key-Value Coding (KVC). KVC is a way of accessing an objects’s properties indirectly, using strings to identify properties, rather than through direct invocation of accessor methods or instance variables.

KVO and KVC rely on the Objective-C runtime, which means that only classes that inherit from NSObject and mark their properties with @objc and dynamic modifiers can use them. The @objc attribute exposes the property to the Objective-C runtime, and the dynamic modifier tells the compiler to use dynamic dispatch to access the property, rather than static dispatch. This dynamic dispatch system allows KVO to insert itself into the property access path transparently.

When you observe an object using KVO, the Objective-C runtime dynamically creates a subclass of the object’s class, and overrides the setter of the observed property. The overridden setter includes notifying the observer object about the change, and calling the original setter of the property. The runtime then changes the isa pointer of the object to point to the dynamic subclass, rather than the original class.

This isa pointer is a special pointer that every Objective-C object has. It points to the class object that describes the type of the instance. This class object contains all the necessary information about the class, such as its superclass, its protocols, and the methods and properties that instances of this class can use

When the property value changes, the observer is automatically notified.

To start observing, an observer registers itself with the target object for a specific property key path. This registration process involves specifying the object to observe, the property to observe (via key path), and a block or method to be executed when the observed property changes.

An Example

Let’s examine a practical example where we want to observe for changes in a user’s account balance within a banking app. We define an Account class that will have a balance property we want to observe. This class inherits from NSObject and marks balance as @objc dynamic.

class Account: NSObject {
    @objc dynamic var balance: Double
    
    init(balance: Double) {
        self.balance = balance
    }
    
    // A method to simulate deposit
    func deposit(amount: Double) {
        balance += amount
    }
}

In order to observe for the property changes, we need to store the observer object returned by observe(_:options:changeHandler:) method to keep observing.

class BankViewController: UIViewController {
    // Starting with an initial balance
    var account: Account = Account(balance: 1000.00)
    var balanceObservation: NSKeyValueObservation?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Start observing the balance property
        balanceObservation = account.observe(\.balance, options: [.old, .new]) { account, change in
            print("Balance changed from \(change.oldValue!) to \(change.newValue!)")
        }
        
        // Simulate a deposit to test KVO
        account.deposit(amount: 500.00)
    }
}

Key Points to Remember

  • It’s crucial to manage the lifecycle of your observers carefully. If an observer is deallocated while still registered, it can lead to crashes. Make sure to store the return value of the observe method and invalidate it if necessary.
  • Notifications are not guaranteed to be delivered on any specific thread unless explicitly specified. Handle thread management according to your needs.
  • Since KVO relies on the Objective-C runtime, it doesn’t work with pure Swift types that do not inherit from NSObject.

Conclusion

While KVO can be powerful, overuse can lead to code that’s hard to debug and maintain. Consider using Swift’s native features like property observers (willSet and didSet), delegates, or closure-based callbacks for simpler scenarios. If you need more complex data-binding, especially in SwiftUI apps, consider using the Combine framework, which offers a more robust and type-safe approach to observing changes.

Thanks for reading! 🚀

Categorized in: