Search code examples
cocoamacoscore-datansoutlineviewnstreecontroller

How do I use NSTreeController, NSOutlineView and Core Data with an "invisible" root item?


I have a Core Data model which consists of a simple tree of a particular entity, which has two relationships, parent and children. I have an NSTreeController managing the model, with an NSOutlineView bound to the NSTreeController.

My problem is that I need a single root object, but this should not display in the outline view, only its children should be displayed at the top level of the outline view. If I set the fetch predicate of the NSTreeController in Interface Builder to parent == nil, everything works fine except the root item is visible as the top level item in the outline view.

My entity has an attribute, isRootItem, that is true for the root item only.

My model looks like this:

Node 1
|
+-Node 2
  |   |
  |   +-Node 5
  |
  Node 3
  |
  Node 4

The outline view should look like this:

Outline View Image
(source: menumachine.com)

I need to display Nodes 2, 3 and 4 at the top level of the outline view (Node 1 should not be visible), but still have their parent be "Node 1". Node 1 has a value of YES for isRootItem and all the others have NO.

If I set the fetch predicate of the tree controller to parent.isRootItem == 1, this displays the tree correctly, but as soon as I add a new item to the top level it fails because the tree controller does not assign the "invisible" root item as the parent of the new item.

Is there a way to get the NSTreeController/NSOutlineView combination to work in this situation?


Solution

  • What I've ended up doing is subclassing NSTreeController and overriding -insertObject:atArrangedObjectIndexPath: to directly set the parent to my root object if the object being inserted is being inserted at the top level of the tree. This seems to work reliably.

    Obviously more work would be needed to handle moving items and inserting multiple items but this seems to be the best way forward.

    - (void)insertObject:(id)object atArrangedObjectIndexPath:(NSIndexPath *)indexPath
    {
        NodeObject* item = (NodeObject*)object;
        //only add the parent if this item is at the top level of the tree in the outline view
        if([indexPath length] == 1)
        {
            //fetch the root item
            NSEntityDescription* entity = [NSEntityDescription entityForName:@"NodeObject" inManagedObjectContext:[self managedObjectContext]];
            NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init]; //I'm using GC so this is not a leak
            [fetchRequest setEntity:entity];
            NSPredicate* predicate = [NSPredicate predicateWithFormat:@"isRootItem == 1"];
            [fetchRequest setPredicate:predicate];
    
            NSError* error;
            NSArray* managedObjects = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error];
    
            if(!managedObjects)
            {
                [NSException raise:@"MyException" format:@"Error occurred during fetch: %@",error];
            }
    
            NodeObject* rootItem = nil;
            if([managedObjects count])
            {
                rootItem = [managedObjects objectAtIndex:0];
            }
            //set the item's parent to be the root item
            item.parent = rootItem;
        }
        [super insertObject:object atArrangedObjectIndexPath:indexPath];
        //this method just sorts the child objects in the tree so they maintain their order
        [self updateSortOrderOfModelObjects];
    }