• Preen and Prune

    Adding Drag support to an NSTreeController with Core Data
    Part 1 An Alphabetically Ordered Tree


    Introduction

    This article describes the trials and tribulations of using an NSOutlineView with an NSTreeController and core data and supporting drag operations.

    Currently I'm working on adding sub-categories to Laughing Man Factorial. I found adding drag support to a tree to be one of the more difficult things to do. I also found that most of the information I collected was scattered around in places so I figured it was time to give back by putting all this in one place.

    Unordered versus Ordered Trees

    An unordered tree allows the user to store elements in any chosen order.


    An unordered tree

    Users are quite accepting of groupings that are only in alphabetcial order, the filesystem, or most common mail clients (Apple's mail, and Microsoft Outlook) are common examples. Have you ever prefixed a name with "_" to get it to show up at the top?.


    An ordered tree

    In effect both trees are actually ordered, but to create a tree that is not ordered by the viewable name we add a hidden variable and do some acrobatics to keep the order the user chooses.

    This portion of this article will explain how to hook up everything and create an ordered tree that supports dragging. The second part will expand on this by adding support for an unordered tree. This article assumes that you have some familiarity with core data and bindings. Cocoa Dev Central has some good primers if you need to get up to speed.

    Follow the instructions below or download a completed version of the project and poke at it.


    Download Project

    To start create a new project "Core Data Application".

    Create the Core Data Object (Some Fluff)

    Create a group entity that looks like the image below.


    • Name is required
    • Both relationships are optional.
    • subGroup is a To-Many relationship while parent is not.
    • The inverse relationship to parent is subGroup
    • Set the default value of name to "new group".

    Interface Builder Setup (More Fluff)

    In Interface Builder create an NSArrayController


    • Set it's mode to Entity and the entity name to group
    • Set it's predicate to "parent == nil", and be sure to click the "Set Predicate" button so that it is saved
    • Check "Automatically prepares content"
    • Bind the managedObjectContext

    In Interface Builder create an NSTreeController


    • Set it's mode to Entity and the entity name to group
    • Set it's children key path to subGroup
    • Check "Automatically prepares content"
    • Bind it's content array to the NSArrayController's arranged objects

    In Interface Builder drag an NSOutlineView onto the window


    • Delete the second table column and widen the one that remains
    • Bind the remaining NSTableColumn to the NSTreeController's arrangedObjects, use a Model Key Path of name

    Add a button to the window and connect it to the add: action of the NSArrayController.

    You can compile and run the project now. Add new groups by clicking on the add button, and rename them by double clicking on the row and editing the text. You can't drag anything yet and you're not guaranteed of the order the data will be after each load. Try it! Add a couple of items, run and close the application several times. You will discover that even with a small number of entries (say three), the order is never guaranteed.

    Setting up Drag Support (The Meat)

    Create a class called DragController that extends NSObject in interface builder. Add the outlets shown in the image below, then instantiate the class, connect the outlets and create the files.


    Set the dataSource of the outlineView to the DragController you just created.


    Now make your header file look like the listing below.

    DragController.h
    @interface _NSArrayControllerTreeNode : NSObject { // opaque } - (unsigned int)count; - (id)observedObject; - (id)parentNode; - (id)nodeAtIndexPath:(id)fp8; - (id)subnodeAtIndex:(unsigned int)fp8; - (BOOL)isLeaf; - (id)indexPath; - (id)objectAtIndexPath:(id)fp8; @end @interface DragController : NSObject { IBOutlet NSTreeController *groupTreeControl; IBOutlet NSOutlineView *treeTable; NSArray* dragType; _NSArrayControllerTreeNode* draggedNode; } @end

    So what's going on here? Basically objects returned in the dataSource methods (which we will implement next) will return _NSArrayControllerTreeNode objects, a private controller class. You don't have to declare this interface, you can just call the observedObject method on an (id) reference to get the relevant managed object. However, if you like to turn "the treat warnings as errors flag" on like I do then this is the best way. For a detailed explanation check out the references at the bottom of the page. Now lets look at the implemenation file.

    DragController.m
    @implementation DragController - (void)awakeFromNib { dragType = [NSArray arrayWithObjects: @"factorialDragType", nil]; [ dragType retain ]; [ treeTable registerForDraggedTypes:dragType ]; NSSortDescriptor* sortDesc = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES]; [groupTreeControl setSortDescriptors:[NSArray arrayWithObject: sortDesc]]; [ sortDesc release ]; } - (BOOL) outlineView : (NSOutlineView *) outlineView writeItems : (NSArray*) items toPasteboard : (NSPasteboard*) pboard { [ pboard declareTypes:dragType owner:self ]; // items is an array of _NSArrayControllerTreeNode draggedNode = [ items objectAtIndex:0 ]; return YES; } - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(int)index { _NSArrayControllerTreeNode* parentNode = item; NSManagedObject* draggedTreeNode = [ draggedNode observedObject ]; [ draggedTreeNode setValue:[parentNode observedObject ] forKey:@"parent" ]; return YES; } - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(int)index { _NSArrayControllerTreeNode* newParent = item; // drags to the root are always acceptable if ( newParent == NULL ) { return NSDragOperationGeneric; } // Verify that we are not dragging a parent to one of it's ancestors // causes a parent loop where a group of nodes point to each other // and disappear from the control NSManagedObject* dragged = [ draggedNode observedObject ]; NSManagedObject* newP = [ newParent observedObject ]; if ( [ self category:dragged isSubCategoryOf:newP ] ) { return NO; } return NSDragOperationGeneric; } - (BOOL) category:(NSManagedObject* )cat isSubCategoryOf:(NSManagedObject* ) possibleSub { // Depends on your interpretation of subCategory .... if ( cat == possibleSub ) { return YES; } NSManagedObject* possSubParent = [possibleSub valueForKey:@"parent"]; if ( possSubParent == NULL ) { return NO; } while ( possSubParent != NULL ) { if ( possSubParent == cat ) { return YES; } // move up the tree possSubParent = [possSubParent valueForKey:@"parent"]; } return NO; } // This method gets called by the framework but // the values from bindings are used instead - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { return NULL; } /* The following are implemented as stubs because they are required when implementing an NSOutlineViewDataSource. Because we use bindings on the table column these methods are never called. The NSLog statements have been included to prove that these methods are not called. */ - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { NSLog(@"numberOfChildrenOfItem"); return 1; } - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { NSLog(@"isItemExpandable"); return NO; } - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item { NSLog(@"child of Item"); return NULL; } @end

    In the awakFromNib method we create a sortDescriptor based on the name attribute. This will give us an ordered tree. The dragging logic is in the writeItems and acceptDrop methods. You'll also notice that most of the other methods are stubs, they aren't used (because the bindings we setup override them) but if you don't implement them you will get runtime errors

    Conclusion

    So now you've used core data, hooked it up to an NSTreeController/OutlineView, and added drag support by using an unsupported private class. In the second part of this tutorial I'll expand on this by explaining how to implement an unordered draggable tree using core data and bindings and the acrobatic code to go along with it.

    References

    Check out part 2 of this article on unordered trees.