I've been trying to find an example, but what I've seen doesn't work in my case.
What would be the equivalent of the following code:
object.addObserver(self, forKeyPath: "keyPath", options: [.new], context: nil)
override public func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
}
The code above works, but I get a warning from SwiftLink:
Prefer the new block based KVO API with keypaths when using Swift 3.2 or later.
I appreciate it if you can point me in the right direction.
Swift 4 introduced a family of concrete Key-Path types, a new Key-Path Expression to produce them and a new closure-based observe function available to classes that inherit NSObject
.
Using this new set of features, your particular example can now be expressed much more succinctly:
self.observation = object.observe(\.keyPath) {
[unowned self] object, change in
self.someFunction()
}
Types Involved
observation:
NSKeyValueObservation
change:
NSKeyValueObservedChange
\.keyPath
: An instance of a KeyPath class produced at compile time.
Key-Path grammar
The general grammar of a Key-Path Expression follows the form \Type.keyPath
where Type
is a concrete type name (incl. any generic parameters), and keyPath
a chain of one or more properties, subscripts, or optional chaining/forced unwrapping postfixes. In addition, if the keyPath's Type can be inferred from context, it can be elided, resulting in a most pithy \.keyPath
.
These are all valid Key-Path Expressions:
\SomeStruct.someValue
\.someClassProperty
\.someInstance.someInnerProperty
\[Int].[1]
\[String].first?.count
\[SomeHashable: [Int]].["aStringLiteral, literally"]!.count.bitWidth
Ownership
You're the owner of the NSKeyValueObservation
instance the observe
function returns, meaning, you don't have to addObserver
nor removeObserver
anymore; rather, you keep a strong reference to it for as long as you need your observation observing.
You're not required to invalidate()
either: it'll deinit
gracefully. So, you can let it live until the instance holding it dies, stop it manually by nil
ing the reference, or even invoke invalidate()
if you need to keep your instance alive for some smelly reason.
Caveats
As you may have noticed, observation still lurks inside the confines of Cocoa's KVO mechanism, therefore it's only available to Obj-C classes and Swift classes inheriting NSObject
(every Swift-dev's favorite type) with the added requirement that any value you intend to observe, must be marked as @objc
(every Swift-dev's favorite attribute) and declared dynamic
.
That being said, the overall mechanism is a welcomed improvement, particularly because it manages to Swiftify observing imported NSObjects
from modules we may happen to be required to use (eg. Foundation
), and without risking weakening the expressive power we work so hard to obtain with every keystroke.
As a side-note, Key-Path String Expressions are still required to dynamically access NSObject
's properties to KVC or call value(forKey(Path):)
Beyond KVO
There's much more to Key-Path Expressions than KVO. \Type.path
expressions can be stored as KeyPath
objects for later reuse. They come in writable, partial and type-erased flavors. They can augment the expressive power of getter/setter functions designed for composition, not to mention the role they play in allowing those with the strongest of stomachs to delve into the world of functional concepts like Lenses and Prisms. I suggest you check the links down below to learn more about the many development doors they can open.
Links:
Key-Path Expression @ docs.swift.org
KVO docs @ Apple
Swift Evolution Smart KeyPaths proposal
Ole Begemann's Whats-new-in-Swift-4 playground with Key-Path examples
WWDC 2017 Video: What's New in Foundation 4:35 for SKP and 19:40 for KVO.
To add something to the answer as I experienced crashes on my app when using this method in iOS 10.
In iOS 10, you still need to remove the observer before deallocating the class or otherwise you will get a crash NSInternalInconsistencyException
stating that:
An instance A
of Class C
was deallocated while key value observers were still registered with it.
To avoid this crash. Simply set the observer property that you're using to nil
.
deinit {
self.observation = nil
}