Search code examples
iosswiftsingletonkvc

Using KVC with Singleton pattern


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


Solution

  • 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:

    1. The shared property does not require @objc qualifier; only the property being observed needs that; and

    2. The init method really should be calling super; and

    3. 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
    }