My questionwas about if it was possible to use KVC on a Singleton property on Swift. I was testing KVC on a class was able to get it working but decided to see if it work on a Singleton class.
I'm running into an error stating that the "shared" property of my Singleton isn't KVC-compliant.
class KVOObject: NSObject {
@objc static let shared = KVOObject()
private override init(){}
@objc dynamic var fontSize = 18
}
override func viewDidLoad() {
super.viewDidLoad()
addObserver(self, forKeyPath: #keyPath(KVOObject.shared.fontSize), options: [.old, .new], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(KVOObject.shared.fontSize) {
// do something
}
}
I am currently getting the error below:
NetworkCollectionTest[9714:452848] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ addObserver: forKeyPath:@"shared.fontSize" options:3 context:0x0] was sent to an object that is not KVC-compliant for the "shared" property.'
The key path is not correct. It’s KVOObject.fontSize
. And you need to add the observer to that singleton:
KVOObject.shared.addObserver(self, forKeyPath: #keyPath(KVOObject.fontSize), options: [.old, .new], context: nil)
As an aside, (a) you should probably use a context to identify whether you're handling this or whether it might be used by the superclass; (b) you should call the super
implementation if it's not yours; and (c) make sure to remove the observer on deinit
:
class ViewController: UICollectionViewController {
private var observerContext = 0
override func viewDidLoad() {
super.viewDidLoad()
KVOObject.shared.addObserver(self, forKeyPath: #keyPath(KVOObject.fontSize), options: [.new, .old], context: &observerContext)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &observerContext {
// do something
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
deinit {
KVOObject.shared.removeObserver(self, forKeyPath: #keyPath(KVOObject.fontSize))
}
...
}
Or, if in Swift 4, it's now much easier as it's closure-based (avoiding need for context) and is automatically removed when the NSKeyValueObservation
falls out of scope:
class ViewController: UICollectionViewController {
private var token: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
token = KVOObject.shared.observe(\.fontSize, options: [.new, .old]) { [weak self] object, change in
// do something
}
}
...
}
By the way, a few observations on the singleton:
The shared
property does not require @objc
qualifier; only the property being observed needs that; and
The init
method really should be calling super
; and
I'd probably also declare it to be final
to avoid confusion that can result in subclassing singletons.
Thus:
final class KVOObject: NSObject {
static let shared = KVOObject()
override private init() { super.init() }
@objc dynamic var fontSize: Int = 18
}