extension NSObjectProtocol where Self: NSObject { func observe(_ keyPath: KeyPath, onChange: @escaping (Value) -> ()) -> NSKeyValueObservation { return observe(keyPath, options: [.initial, .new]) { _, change in // TODO: change.newValue should never be `nil`, but when observing an optional property that's set to `nil`, then change.newValue is `nil` instead of `Optional(nil)`. This is the bug report for this: https://bugs.swift.org/browse/SR-6066 guard let newValue = change.newValue else { return } onChange(newValue) } } func bind(_ sourceKeyPath: KeyPath, to target: Target, at targetKeyPath: ReferenceWritableKeyPath) -> NSKeyValueObservation { return observe(sourceKeyPath) { target[keyPath: targetKeyPath] = $0 } } } // ### Usage class TestViewModel: NSObject { @dynamic var title: String = "Test" } class TestViewController: UIViewController { var observations: [NSKeyValueObservation] = [] override func viewDidLoad() { super.viewDidLoad() observations = [ viewModel.bind(\.title, to: navigationItem, at: \.title) ] }