Search code examples
objective-cxcodedrag-and-dropnsoutlineview

NSOutlineView drag&drop (next to an item) doesn't work for complex items


My basic sample works well for item represented by standard ObjC classes. See initialisation of self.list in sample below:

- (void) updateViews;
{
    NSDictionary *firstParent = [NSDictionary dictionaryWithObjectsAndKeys:@"Foo",@"parent",[NSArray arrayWithObjects:@"Foox",@"Fooz", nil],@"children", nil];
    NSDictionary *secondParent = [NSDictionary dictionaryWithObjectsAndKeys:@"Bar",@"parent",[NSArray arrayWithObjects:@"Barx",@"Barz", nil],@"children", nil];
    self.list = [NSArray arrayWithObjects:firstParent,secondParent, nil];

    self.outlineView.delegate = self;
    self.outlineView.dataSource = self;


    // Enable Drag and Drop
    [self.outlineView registerForDraggedTypes:@[LOCAL_REORDER_PASTEBOARD_TYPE]];
}


#pragma mark - NSOutlineViewDelegate

- (BOOL) outlineView:(NSOutlineView*)outlineView isItemExpandable:(id)item
{
    if ([item isKindOfClass:[NSDictionary class]]) {
        return YES;
    }
    else {
        return NO;
    }
}

- (NSInteger) outlineView:(NSOutlineView*)outlineView numberOfChildrenOfItem:(id)item
{
    if (item == nil) { //item is nil when the outline view wants to inquire for root level items
        return [self.list count];
    }

    if ([item isKindOfClass:[NSDictionary class]]) {
        return [[item objectForKey:@"children"] count];
    }

    return 0;
}

- (id) outlineView:(NSOutlineView*)outlineView child:(NSInteger)index ofItem:(id)item
{
    if (item == nil) { //item is nil when the outline view wants to inquire for root level items
        return [self.list objectAtIndex:index];
    }

    if ([item isKindOfClass:[NSDictionary class]]) {
        return [[item objectForKey:@"children"] objectAtIndex:index];
    }

    return nil;
}

- (NSView*) outlineView:(NSOutlineView*)outlineView viewForTableColumn:(NSTableColumn*)tableColumn item:(id)item
{
    NSTableCellView *result = [outlineView makeViewWithIdentifier:@"DataCell" owner:self];

    NSString *txt = nil;
    if ([item isKindOfClass:[NSDictionary class]]) {
        txt = [item objectForKey:@"parent"];//[NSString stringWithFormat:@"%i kids", [[item objectForKey:@"children"] count]];
    }
    else {
        txt = str(item);
    }

    result.textField.stringValue = txt;
    return result;
}

- (BOOL) outlineView:(NSOutlineView*)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pasteboard 
{
    [pasteboard declareTypes:[NSArray arrayWithObject:LOCAL_REORDER_PASTEBOARD_TYPE] owner:self];
    [pasteboard setData:[@"just a test - not yet implemented" dataUsingEncoding:NSUTF8StringEncoding] forType:LOCAL_REORDER_PASTEBOARD_TYPE];
    return YES;
}

- (NSDragOperation) outlineView:(NSOutlineView*)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index 
{
    NSUInteger op = NSDragOperationNone;
    if (index != NSOutlineViewDropOnItemIndex){
        op = NSDragOperationMove;
    }
    return op;
}

Drag and drop works as expected.

enter image description here

Once I modify the child:(NSInteger)index ofItem:(id)item method to return complex item of my own class, specifically TreeItem then the drag & drop only allows:

  1. drop on item

enter image description here

  1. drop next to parent item

enter image description here

But the drop next to leaf item as shown on the very first picture is not possible. Any clue why?

Let me also add sample code of not-working complex item. Storyboard and dragDrop code is the same for both cases.

#define LOCAL_REORDER_PASTEBOARD_TYPE @"LOCAL_REORDER_PASTEBOARD_TYPE"
#define str(...) [@[__VA_ARGS__] componentsJoinedByString:@""]

@interface TreeItem : NSObject
@property (nonatomic) NSArray<TreeItem*> *children;
@property (nonatomic) NSString *name;
+ (nonnull instancetype) itemWithName:(nonnull NSString*)name;
@end

@implementation TreeItem
+ (nonnull instancetype) itemWithName:(nonnull NSString*)name;
{
    TreeItem *obj = [[self alloc] init];
    obj.name = name;
    return obj;
}
- (NSArray<TreeItem*>*) children;
{
    return self.name.length > 3 ? nil : @[
        [TreeItem itemWithName:str(self.name, @"x")],
        [TreeItem itemWithName:str(self.name, @"z")],
    ];
}
@end

@interface SampleVC ()
@property (weak) IBOutlet NSOutlineView *outlineView;
@property (strong) NSArray<TreeItem*> *treeList;
@end

@implementation SampleVC

- (void) updateViews;
{
    self.treeList = @[
        [TreeItem itemWithName:@"Foo"],
        [TreeItem itemWithName:@"Bar"],
    ];
    self.outlineView.delegate = self;
    self.outlineView.dataSource = self;
    [self.outlineView reloadData];
    [self.outlineView registerForDraggedTypes:@[LOCAL_REORDER_PASTEBOARD_TYPE]];
}

- (BOOL) outlineView:(NSOutlineView*)outlineView isItemExpandable:(id)item
{
    return item == nil ? YES : ((TreeItem*)item).children != nil;
}

- (NSInteger) outlineView:(NSOutlineView*)outlineView numberOfChildrenOfItem:(id)item
{
    return item == nil ? self.treeList.count : (((TreeItem*)item).children != nil ? ((TreeItem*)item).children.count : 0);
}

- (id) outlineView:(NSOutlineView*)outlineView child:(NSInteger)index ofItem:(id)item
{
    return item == nil ? [self.treeList objectAtIndex:index] : ((TreeItem*)item).children[index];
}

- (NSView*) outlineView:(NSOutlineView*)outlineView viewForTableColumn:(NSTableColumn*)tableColumn item:(id)item
{
    NSTableCellView *result = [outlineView makeViewWithIdentifier:@"DataCell" owner:self];
    result.textField.stringValue = ((TreeItem*)item).name;
    return result;
}
@end

Solution

  • The outline view can't find the row of the children and draws the drop indicator at the top of the view. NSOutlineView's rowForItem: can't work if TreeItem returns a new array of new TreeItems each time - (NSArray<TreeItem*>*) children; is called.