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 TreesAn 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.
In Interface Builder create an NSArrayController
In Interface Builder create an NSTreeController
In Interface Builder drag an NSOutlineView onto the window
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 ConclusionSo 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. |