NSTreeController behavior question

It's been a while since I've had to tangle with an NSArrayController or an NSTreeController, and this isn't really spelled out anywhere... and it MAY be the source of one of my issues, so i thought I'd ask rather than just jump on it, only to find out much later that it's no longer an issue.


in my experience with NSArrayController, it seemed that if you want NSArrayController to know that you have added an object to your array once the ArrayController is bound to the array, You can only add that object by doing it through methods in the arrayController. It's a safe assumption that the same is true for NSTreeController, right? nothing's changed, right? for example :


you have an object that is instantiated in the Xib, it's got this array : @objc dynamic var boxes : [BKBoxClass] = []

and your array controller is bound to it.

if you do this in code:

boxes += [BKBoxClass()]


the arrayController would never update it's contents, right? and the same thing would be true for an NSTreeController?


instead, you'd have to do this, right? :

arrayController.addObject(BKBoxClass())

Accepted Reply

the solution, it seems...

is to abandon the use of the NSTreeController entirely.

and instead provide the ancient dataSource, and delegate to the outline view.


what an awful end to an exploration of a ui toolset.

Replies

Yes and no.


>> You can only add that object by doing it through methods in the arrayController


No, that's only one of two ways. The other, more fundamental way is to update your data model (the array controller's content) KVO compliantly.


>>if you do this in code:

>>boxes += [BKBoxClass()]

>>the arrayController would never update it's contents, right?


Yes, because that's Doing It Wrong™. You have to do the update KVO compliantly, and then the array controller will "see" the update via the magic of KVO. I'm not going to give the whole long story about KVO compliant indexed (i.e. array) properties unless I have to, because you're working in Swift, and it's an Obj-C mechanism that means you might have to work directly with the NSArray type, rather than a pure Swift array. But in short, you need to implement the insert, replace and remove accessor methods defined by KVC (https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/KeyValueCoding/index.html), then you can use those directly, or via the mutable array proxy object (https://developer.apple.com/documentation/objectivec/nsobject/1416339-mutablearrayvalue) to do KVO compatible updates using any NSMutableArray API.


>> the same thing would be true for an NSTreeController?


No, not necessarily. The answer depends on how you structure the data model that provides the NSTreeController content. If you use a hierarchy of NSTreeNode objects (which is a really good way, since that's really what NSTreeNode is for), then structural changes made via the "mutableChildren" property of the nodes are KVO compliant.


Conceptually, it might seem easier to use the update methods in NSArrayController and/or NSTreeController. That might be true in a lot of cases, but you have to be careful, because the whole point of NS…Controller objects is to sort and filter your data model. Finding the correct structural alterations to make can be a bit tricky.


Figuring out how to handle all of this smoothly in Swift would be a great project, but I suspect that anything complicated about KVO is going to be hard to get right in Swift, just because the APIs aren't very Swift-natural.

Hi QuinceyMorris,

Actually, I do not find that your answer lines up with what I am experiencing.

I made a simple case project... I'll just post the code, it'll be quicker.


import Cocoa
class ViewController: NSViewController {
    @objc dynamic var objs : [BKObj] = []
   
   
   
    override func viewDidLoad() {
        super.viewDidLoad()
        /
        objs = [BKBranch("one", [] ), BKObj("deadEnd"), BKBranch("second", [BKBranch("int", [BKObj("test"), BKObj("foo"), BKObj("tres")]), BKObj("sveldt"), BKObj("oop"), BKObj("pool")])]
    }
    override var representedObject: Any? {
        didSet {
        /
        }
    }
   
    @IBAction func addStuff(sender: Any){
        objs = [BKObj("replacement")]
    }
}
class BKObj : NSObject{
    @objc dynamic var isLeaf : Bool{
        get {return true}
        set{}
    }
    @objc dynamic var name : String = "name"
   
   
    init(_ name: String) {
        self.name = name
        super.init()
    }
}
class BKBranch : BKObj{
    @objc dynamic var stuff : [BKObj] = []
    @objc override dynamic var isLeaf : Bool{
        get {return false}
        set{}
    }
    @objc dynamic var count : Int {
        get{
            return stuff.count
        }
        set{}
    }
   
    init(_ name: String,_  stuff : [BKObj]) {
        self.stuff = stuff
        super.init(name)
    }
   
}



so what you see is the VierwController for a mac osX desktop app, and the associated classes. BKBranch and BKObj are the classes that make up the herarchy. NOT specifically NSTreeNodes. you can see that in ViewDidLoad, I populate an array: objs with these objects.

there's an IBAction below that, that I trigger from a button in the ui.

it purposefully does the most blatant thing I could think of to upset the apple cart. a straight setting of the array's contents.


the xib is nothing to write home about. an NSTreeController, and an outlineView. the tree is bound to objs, and the outlineView is bound to the TreeController.


clicking the button, replaces the contents of the array. And the outlineView updates without complaint. Not what I was expecting, and not what I'd expect from your description.

maybe my 'stuff' property is KVO compliant enough. But if all it takes is to put @objc and dynamic in front of the property, then it's really no big deal, it's almost automatic.


but none of that is helping me solve my issues in my main app. All this tells me is that it's probably not anything to do with setting the contents of the array. I've checked to see that the Array exists,a nd that it gets populated, and that I can observe these changes. But for some reason I have not yet pinned down, my binding either from the TreeCon to the array, or from the outlineView to the TreeCon, just isn't working.

>> maybe my 'stuff' property is KVO compliant enough


If you replace the entire array, you're just doing a simple property "set", which is KVO compliant by default, like any normal setter. However, if you try to insert, replace or remove elements from the array, the notifications won't be generated.


For the main app, it's hard to pin down what's wrong or where to look. You just have to keep looking.

so I've pushed forward a bit.


I ripped out everything in the main app, and built an example in situ of the simple example. And no problem.

I substituted back into that mix my original array... nothing.

then i tried setting my original array, before the UI loads and all of those bindings come into play... with no changes, that object populated in the outlineView.

Then I triggered the change to the array, and everything dissapears again.


it's definetly a KVO issue, but it's different behavior from the simple example. weird.

QuinceyMorris,

Swift treats _ANY_ alterations to the contents of an array as the same thing as replacing all of the contents. I've seen it dozens of times.

This may be a consequence of the bridging between Array<> and NSArray. Remember that when you're dealing with KVO, it's a pure Obj-C mechanism. Anything Swift adds on top is just a complication, which sometimes makes it harder to see what's going on.

time has passed, and I have learned some things, and discovered more questions.


I have discovered a few bugs in my own code, which were the main cause of the failure to display any data. Iwas esentially observing the right array, and then reading from the wrong array. Just a bad bug.

I have adopted NSTreeNode, in the process which lead to discovery of my bugs. And I have issues...

NSTreeNode... appears to set as it's representedObject, another NSTreeNode. This is not correct behavior. I am under no circumstances sending an NSTreeNode as the representedObject in the treeNodes' init method call. But... if you peel back another layer and examine the first Nodes, representedObject's representedObject, you see that the representedObject is indeed the one You set as the representedObject in the first TreeNode.


this is an unfortunate turn of events for someone trying to make a clean binding. a model key path of : representedObject.representedObject.name is full of red flags.


I have seen some chatter in google searches about the general shakiness of the NSTreeController class (dating back to Snow leopard. there is very little on the subject in general.)


Am I seeing a bug here?

I think it's a messy proxy.

binding to the objects produces a memory error.


no worries. NSTreeNode is a catastophie of a class, but it won't take more than a few minutes to completely duplicate it, properly. I'll be right back.

this is disconcerting.

theNSTreeController, forces the objects you send to it, into an Instance of NSTreeNode. which renders the objects you would most like to bind to, into proxy objects... that cannot be bound to because they are transient in nature, and will not exist at the exit of your method.


this is the rough equivalent of fixing a car by blowing it up.

the solution, it seems...

is to abandon the use of the NSTreeController entirely.

and instead provide the ancient dataSource, and delegate to the outline view.


what an awful end to an exploration of a ui toolset.

Well, I think you came to the right answer in the end (don't use NSTreeController), but this wasn't your original question.


FWIW, there's some history here. Many years ago, perhaps about 10 years, NSTreeController used opaque (i.e. private) proxy objects internally to represent the objects in your data model. This was fine, except that there were some situations where you needed to figure out which of your data model (content) objects a proxy represented. This was impossible without some horrible hacks.


So, Apple changed NSTreeController to use NSTreeNode internally, so that you can use the "representedObject" property to find your data model object. This was great. However, if your actual data isn't structured hierarchically like NSTreeController wants, or can't be made to appear so on the fly, you need an intermediate data structure of your own, which you put "between" the actual data and the tree controller. It turns out, NSTreeNode is a really useful class for this too. It's already compatible with the way NSTreeController accesses children and parents, and it's KVO compliant. So, yes, you can end up with two levels of NSTreeNode between your data and your outline view.


It has always worked, and presumably still works, but it does involve some complexity, both in setup and in usage.


It turns out to be much easier (even if a bit more boilerplate code) to just forget about NSTreeController and use NSOutlineViewDataSource and NSOutlineViewDelegate. If that had been the question, this would be the answer. Still, you got there for yourself. 🙂