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