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