KVO


Greetings, traveler!

Key-value observing (KVO) enables objects to be notified of changes to specific properties of another object. KVO is related to the  KVC (Key-Value Coding). You can read more about it here.

While KVO can be useful, some preparations are required to implement it in pure Swift code, as it relies on the Objective-C runtime. Your class must inherit from the NSObject, and you need to mark each property with the @objc dynamic attribute. Despite these limitations, KVO remains a powerful tool in Swift, especially for notifying classes of property changes and updating the user interface accordingly. Let’s consider an example of usage.

Example

First, let’s create our own class.

final class Vehicle: NSObject {
    @objc dynamic var speed: Int = .zero
}

Now, we can create an observer.

final class VehicleObserver: NSObject {
    
    var observation: NSKeyValueObservation?

    func observe(_ vehicle: Vehicle) {
        observation = vehicle.observe(\.speed, options: [.new]) { vehicle, _ in
            dump(vehicle)
        }
    }
    
}

let observer = VehicleObserver()
let vehicle = Vehicle()
observer.observe(vehicle)
vehicle.speed = 100

UIKit example

Since Apple’s UIKit is written in Objective-C, KVO has already been implemented there so you can use it without additional code. Let’s consider an example with a UIScrollView. We can track its contentOffset changes via KVO.

final class ContentOffsetObserver {
    
    var observation: NSKeyValueObservation?
    
    func observe(_ scrollView: UIScrollView) {
        observation = scrollView.observe(\.contentOffset) { scrollView, _ in
            print(scrollView.contentOffset)
        }
    }
    
}

let observer = ContentOffsetObserver()
let scrollView = UIScrollView()

observer.observe(scrollView)

Memory issues: holding reference and weak self

Two things should be mentioned in this context.

First, you should cache the observation reference since it will be released from memory.

Second, if you plan to use self inside the observation closure, you must mark the reference as weak.

final class MyView: UIView {
    
    var scrollView = UIScrollView()
    
    var observation: NSKeyValueObservation?
    
    func observe() {
        observation = scrollView.observe(\.contentOffset) { [weak self] _, _ in
            print(self?.scrollView.contentOffset)
        }
    }
    
}

Conclusion

KVO may look a bit outdated, but it is still a convenient tool for reducing code.