Blog

Swift Tip: Bindings with KVO and Key Paths

In the Model-View-ViewModel chapter of our new book, App Architecture, we use RxSwift to create data transformation pipelines and bindings to UI elements.

However, not everyone can, or wants to use a full reactive framework. With this in mind, we added an example to demonstrate how to create lightweight UI bindings using Key-Value-Observing and Swift’s key paths.

First, we create a wrapper around the KVO API tailored to our use case:

extension NSObjectProtocol where Self: NSObject {
    func observe<Value>(_ keyPath: KeyPath<Self, Value>,
                        onChange: @escaping (Value) -> ()) -> Disposable
    {
        let observation = observe(keyPath, options: [.initial, .new]) { _, change in
            // The guard is because of https://bugs.swift.org/browse/SR-6066
            guard let newValue = change.newValue else { return }
            onChange(newValue)
        }
        return Disposable { observation.invalidate() }
    }
}

When setting up the observation, we specify the .new and .initial options. This means we’ll be called back anytime the value changes, but we’ll also get a callback immediately, which is crucial for bindings. Additionally, we return a Disposable to control the lifetime of the observation. As long as the disposable is alive, the observation is active as well — similar to how reactive libraries handle lifetime management of observations.

Now, we can write the actual binding helper method:

extension NSObjectProtocol where Self: NSObject {
    func bind<Value, Target>(_ sourceKeyPath: KeyPath<Self, Value>,
                             to target: Target,
                             at targetKeyPath: ReferenceWritableKeyPath<Target, Value>) -> Disposable
    {
        return observe(sourceKeyPath) { target[keyPath: targetKeyPath] = $0 }
    }
}

Whenever the value of the property specified by sourceKeyPath changes, we update the value at targetKeyPath on target. With this in place, we can bind our view model’s properties to the views:

override func viewDidLoad() {
    super.viewDidLoad()
    disposables = [
        viewModel.bind(\.navigationTitle, to: navigationItem, at: \.title),
        viewModel.bind(\.hasRecording, to: noRecordingLabel, at: \.isHidden),
        viewModel.bind(\.timeLabelText, to: progressLabel, at: \.text),
        // ...
    ]
}

For the full example, see Chapter 3 of App Architecture.

We develop more interesting uses for Swift’s KeyPath in Swift Talk 75: Auto Layout with Key Paths (a public episode).

The book is currently available through our Early Access program, with a final release in May. To learn more, read our announcement post. 😊


Stay up-to-date with our newsletter or follow us on Twitter.

Back to the Blog

recent posts