nsoutlineviewcyclic-reference

How to handle NSOutlineViewDataSource with loops


I have an NSOutlineView populated from an NSOutlineViewDataSource which is a thin wrapper over a supplied object model, containing a mixture of arrays, dictionaries and primitive types.

However the object model has loops in its hierarchy. I thought that this shouldn't be a problem, as a user would not expand that node (or get bored after a while) but the two different paths refer to the same object instance, what happens to one happens to the other, if the parent is expanded (as it would be) then the child item is also expanded resulting in an infinite loop.

I've tried putting conditional logic into my DataSource, to not return an object if the document dictionary key is "Parent". But all I see in isItemExpandable is a reference to the object, and I have no idea if the key is "Parent" or not.

I've tried caching the objects in an NSDictionary, with their keys, to see if I've encountered them before, this allowed me to determine the key name and return NO for isItemExpandable this partially worked but as they are same object, the parent's object key was overwritten with "Parent" changing the name shown in the NSOutlineView and preventing the "Parent" key from being expanded or collapsed.

The datasource is populated via callbacks, I haven't got much context to determine if the object is the parent node, or the child, let alone if it is referenced multiple times.

I've seen a similar questions refer to NSPathIndex but that appears to be for array indexes and not dictionary keys, NSTreeNode has a similar problem and there doesn't appear to be an NSKeyPath class.

Does anyone know how to handle this case?


Solution

  • Each item in the outline view must be unique. Wrap each node in a NSTreeNode or similar custom class. Repeated nodes are wrapped again. For example:

    MyObject is a NSObject subclass with property children but representedObject can be anything.

    @property NSTreeNode *rootNode;
    
    // setup
    self.rootNode = [[NSTreeNode alloc] initWithRepresentedObject:rootObject];
    [self.outlineView reloadData];
    
    - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(NSTreeNode *)item {
        if (!item)
            item = self.rootNode;
        MyObject *object = item.representedObject;
        return object.children.count;
    }
    
    - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(NSTreeNode *)item {
        if (!item)
            item = self.rootNode;
        MyObject *object = item.representedObject;
        /*
        // show repeated nodes as leaves
        NSTreeNode *parentItem = item.parentNode;
        while (parentItem) {
            if (parentItem.representedObject == object)
                return NO;
            parentItem = parentItem.parentNode;
        }
        */
        return object.children.count > 0;
    }
    
    - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(NSTreeNode *)item {
        if (!item)
            item = self.rootNode;
        if (index >= item.childNodes.count) {
            // create child nodes
            NSMutableArray *childrenArray = item.mutableChildNodes;
            [childrenArray removeAllObjects];
            MyObject *object = item.representedObject;
            for (MyObject *childObject in object.children)
                [childrenArray addObject:[[NSTreeNode alloc] initWithRepresentedObject:childObject]];
        }
        return item.childNodes[index];
    }