KVO + swift question

so go to this link : example

about 2/3rds down the page they have this code:

observation = observe(\.objectToObserve.myDate) { object, change in
  //do stuff
}


the code editor there messed up the formatting, and is currently blocking me form editing it. so you'll have to live with the poor formatting or go to the link. It's an example of setting up an NSKeyValueObservation property.

it NEVER works for me if I write the code like that. NEVER. NOT ONCE. And I've tried. tried real hard. It only works if I make modifications:



observation = objectToObserve.observe(\.propertyToObserve, options: [.old, .new]){ (object, change) in{
     // do stuff in here.
}


this is the ONLY phrasing I have ever discoved that will allow an NSKeyValueObservation to instantiate.

Just like that. Only that. you have to include options, you have to provide an array of options, the objectToObserve has to be the Object to call the observe function, 'object, change' MUST be embedded in parenthesese. Any other way and I get a compiler error : Type of expression is ambiguous without more context. I wish to understand it, so that I can track down a bigger problem.


after hundreds of successful instantiations of Observers I have one that is instantiated, but never gets notified of changes to the watched object. So I'm suspicious that I my necessary formatting changes have introduced unwanted wrinkles.


here's an In situ example of the method that isn't working:

func shapeLayerAdded(_ layerCon: MDShapeLayerController){
        if layerCon.primitives.count > 0{
            self.addPrimWidgets(layerCon.primitives, forLayerCon: layerCon)
        }

        let conObsrvr = layerCon.observe(\.primitives, options: [.old, .new]){ (object, change) in
            print("we get the observation")// this never gets called.
            if let newArray = change.newValue , let oldArray = change.oldValue{
                let addedItems = newArray.filter({ oldArray.contains($0) == false})
                let removedItems = oldArray.filter({ newArray.contains($0) == false})
       
                if addedItems.count > 0 {
                    self.addPrimWidgets(addedItems, forLayerCon: layerCon)
                }
       
                if removedItems.count > 0 {
                    self.removePrimWidgets(removedItems)
                }
            }
        }

        contentObsrvrs[layerCon] = conObsrvr // at this point I can confirm that the observer is instantiated and added to the dictionary.
    }


my suspicion is that I'm introducing unwanted complexity to the observation by calling the observe function through the objectToObserve, and possibly that is interfering with properly getting notification. As I've said before: I have hundreds of examples of this code working. It's gotten to the point that I can write it in my sleep. But whatever. I know the official examples show it another way, so there might be an issue with the way I am doing it. But, If I were to change it to the official 'blessed' phrasing...

that is a compiler error right there. it won't compile.

The objectToObserve is a descendant of NSObject, and the property we are observing is KVO aware

the MDPrimitive class is an NSClass subclass, and ALL of it's properties are KVO aware.


so... my question is twofold:

1. what is neccessary in order to instantiate an NSKeyValueObservation in the way described in that link? in this current code example I am doing so inside a function, passing the objectToObserve as an argument in the function. Could that be interfering with the code?


2. I've provided all of my offending code. is there anything obvious that jumps out aside of the KeyValueObservation code? I've done tests in a playground to make sure Observers do get notification when I make the kinds of changes I am making to the ObservedObject's Observed property.

Replies

I solved the pressing issue... but I am still curious to find out why the official "blessed" approach to instantiating an NSKeyValueObservation just doesn't work.

With the code in the link you have shown, KVO works perfectly as expected:

class MyObjectToObserve: NSObject {
    @objc dynamic var myDate = NSDate()
    func updateDate() {
        myDate = NSDate()
    }
}
class MyObserver: NSObject {
    @objc var objectToObserve: MyObjectToObserve
    var observation: NSKeyValueObservation?

    init(object: MyObjectToObserve) {
        objectToObserve = object
        super.init()

        observation = observe(\.objectToObserve.myDate) { object, change in
            print("Observed a change to \(object.objectToObserve).myDate, updated to: \(object.objectToObserve.myDate)")
        }
    }
}
let observed = MyObjectToObserve()
let observer = MyObserver(object: observed)
observed.updateDate()
//->Observed a change to <KVOExample.MyObjectToObserve: 0x101022760>.myDate, updated to: 2018-03-23 22:54:22 +0000

I do not understand why you say it NEVER works. But I guess you have not tested the exact code in the link.


This code:

        observation = observe(\.objectToObserve.myDate) { object, change in
            print("Observed a change to \(object.objectToObserve).myDate, updated to: \(object.objectToObserve.myDate)")
        }


is a short-circuite form of this:

        observation = self.observe(\MyObserver.objectToObserve.myDate) { object, change in
            print("Observed a change to \(object.objectToObserve).myDate, updated to: \(object.objectToObserve.myDate)")
        }

(Remember `MyObserver` is the type of `self`.)


When you write `anObject.observe(...)`, the keypath passed to the method needs to be for the class of anObject.

And any intermediate properties included in the keypath needs to be annotated with `@objc`. As in the example code:

@objc var objectToObserve: MyObjectToObserve

And the final target of the observation needs to be `dynamic` as well as `@objc`:

@objc dynamic var myDate = NSDate()


I cannot find if your `\MDShapeLayerController.primitives` fulfills the requirements as I do not know how `MDShapeLayerController` is defined.

Have you checked it?

testing the exact same code front to back is guesswork. being able to duplicate the results is knowlege.


.self understood is an old OOP concept I am intimately familiar with, but you seem to be saying (and moving right past it very quickly) that in order to observe a property, one has to call the observe method for the object that owns the property.

an example:

2 objects : aController, and anObject. anObject has a properly formatted and KVO aware property named : theProperty.

what it seems you are saying is that the appropriate way to observe theProperty from aController is like this:

observation = anObject.observe( // rest of code for observation

I don't want to put words into your mouth, so please elaborate/correct as nec.

but this is central to my issue, everything else, is window dressing.


when I observe a property of a class from inside that class, self is understood. additionally the KeyPath does not require the Object type. for instance:

\.objectToObserve.propertyToObserve

is unneccessary. I'm not certain that doing so will cause an error, I will have to test that. But in my experience, if you are observing a property in another object, using that phrasing does indeed incur an error. This is what works universally:

\.propertyToObserve


so, if you must use the local Observation method for the object you are going to observe (and I am not certain of this, it's something I think I may have gleaned form the way you wrote... feel free to correct this) and you can't use the ObjectToObserve in the property path in that case... and it's unneccessary (and potentially a source of a compiler error... as of yet untested) when you are observing a property in your class (which, i would like to point out is a redundancy in swift in any case) then we have described a situation where writing code in this way:

observation = observe(\.objectToObserve.propertyToObserve) { object, change in

is strictly prohibited in most cases, and redundant in all of the others.


a couple details on my coding practices, that may be relevant :

all of my classes are subclasses of NSObject.

the properties I observe are always setup with "@objc dynamic"

generally, when I do add observation it can be in types of 2 situations:

1. it's a property in a companion class. when the companion class property in the observing class is set, then I setup observation of individual properties of the campanion class. I never observe properties of properties. there is never a : \.objectToObserve.property.property

2. the observation class mainatins an array of objects which it then observes. For this a separate dictionary is stored of the type [classToObserve : NSKeyValueObservation] and as objects are added or removed form the array, KVO objects are added or removed (and invalidated)


the MDShapeLayerController.primitives is a property in an NSObject subclass. It's primitives property is defined like this:

@objc dynamic var primitives : [MDPrimitive] = []


the specific Issue I was having was solved. It was unrealted to observation, and centered on an issue with NSTreeController. I have since removed the NSTreeController, and built the same functionality from scratch. But it has always bothered me that I did not understand in my bones why my observation code is required to be so different from that of the official example. I'm still bothered by it.


But I have run into something slightly interesting in the mean time.

when observed, this property :

@objc dynamic var cgpath : CGPath!

provides NO notice of any changes. the observer is never triggered.

it was very troubling, and I checked, rechecked everything. In the end I changed it like this:

@objc dynamic var pathUpdated : Bool = true
    @objc dynamic var cgpath : CGPath!{
        didSet{
            pathUpdated = true
        }
    }


and all I did was change the property observed in my observation code:


let obsrvr = each.observe(\.pathUpdated, options: [.new, .old]){ (obj, change ) in
                    print("primitive path updated")
                    let masterPath = CGMutablePath()
                    for each in self.primitives{
                        masterPath.addPath(each.cgpath)
                    }
                    self.shapeLayer.path = masterPath
                }


and everything worked as expected.


one thing I tried previously presented very strange errors. at the point where I set the cgpath, I added willset and didset methods to spoof the notification that changes were made to the property. It caused an error that said that my class was not KVO for the property "cgpath." It clearly was KVO compliant for that property. the error was obviously not telling me what was happeing, but it was my first clue that CGPaths might not be capable of being observed. very wierd.