wannabegeek

Icon

Things I find interesting – generally programming

NSOrderedSet and NSArrayController bindings

Since the introduction of Mac OS X Lion we have been give a new container which can be used for maintaining order in CoreData relationships – NSOrderedSet
One of the first issues I came across with this is that it isn’t bindings compatible with NSArrayController like NSSet and NSArray are. So I spent a while investigating how to work around this issue.

Ordered values bound to NSArrayController

The simplest solution I came up with was to convert it to an NSArray so that the order can be maintained. This can simply be done using a NSValueTransformer sub-class.

Here is the interface

@interface OrderedSetArrayValueTransformer : NSValueTransformer
 
@end

…& the implementation

@implementation OrderedSetArrayValueTransformer
 
+ (Class)transformedValueClass {
    return [NSArray class];
}
 
+ (BOOL)allowsReverseTransformation {
    return YES;
}
 
- (id)transformedValue:(id)value {
    return [(NSOrderedSet *)value array];
}
 
- (id)reverseTransformedValue:(id)value {
	return [NSOrderedSet orderedSetWithArray:value];
}
 
@end

Set up this value transformer in the NSArrayController bindings pane in Interface Builder.

Enabling re-ordering of CoreData objects

To enable drag and drop re-ordering of the ordered list is actually again very simple.

I created a class that will act as the NSTableView’s delegate and datasource. This contains a few IBOutlets to keep track of the table view and the array controller.
When the class is loaded from the NIB file, it registers the NSTableView for dragging objects of the entity type contained in the NSArrayController.

Hopefully the rest of the code below is self-explanatory.

#import "DragableOrderedSetTableViewDelegate.h"
 
@implementation DragableOrderedSetTableViewDelegate
 
@synthesize tableView;
@synthesize arrayController;
 
- (void)awakeFromNib {
	[super awakeFromNib];
	// user interface preparation code
 
	[tableView registerForDraggedTypes:[NSArray arrayWithObjects:[self.arrayController entityName], nil]];
	[tableView setDraggingSourceOperationMask:NSDragOperationMove forLocal:YES];
}
 
#pragma mark -
#pragma mark NSTableViewDataSource
 
- (BOOL)tableView:(NSTableView *)tv writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard*)pasteboard {
	NSData *data = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes];
	[pasteboard declareTypes:[NSArray arrayWithObject:[self.arrayController entityName]] owner:self];
	[pasteboard setData:data forType:[self.arrayController entityName]];
	return YES;
}
 
- (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id )info proposedRow:(int)row proposedDropOperation:(NSTableViewDropOperation)op {
	if ([info draggingSource] == tableView) {
		if (op == NSTableViewDropOn)
			[tv setDropRow:row dropOperation:NSTableViewDropAbove];
 
		if ([[[NSApplication sharedApplication] currentEvent] modifierFlags] & NSAlternateKeyMask)
			return NSDragOperationCopy;
		else
			return NSDragOperationMove;
	} else {
		return NSDragOperationNone;
	}
}
 
- (BOOL)tableView:(NSTableView *)aTableView acceptDrop:(id )info row:(int)row dropOperation:(NSTableViewDropOperation)operation {
 
	NSDictionary *bindingInfo = [self.arrayController infoForBinding:@"contentArray"];
	NSMutableOrderedSet *s = [[bindingInfo objectForKey:NSObservedObjectKey] mutableOrderedSetValueForKeyPath:[bindingInfo objectForKey:NSObservedKeyPathKey]];
 
	NSPasteboard *pasteboard = [info draggingPasteboard];
	NSData *rowData = [pasteboard dataForType:[self.arrayController entityName]];
	NSIndexSet *rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:rowData];
	if ([rowIndexes firstIndex] > row) {
		// we're moving up
		[s moveObjectsAtIndexes:rowIndexes toIndex:row];
	} else {
		// we're moving down
		[s moveObjectsAtIndexes:rowIndexes toIndex:row-[rowIndexes count]];
	}
 
	return YES;
}
 
@end

Sample Project

I created a sample project to demonstrate this, it can be downloaded from here: OrderedSetTest.tar.gz

Category: 10.7, AppKit, Cocoa, Desktop, OS X, Programming

Tagged: , ,

8 Responses

  1. Petr says:

    Thanks so much for this! I was trying to do exactly the same for my simple data input application and was unable to find any documentation on it. I even thought of overriding the entity class’s methods to return arrays instead of ordered sets, but your solution is really elegant and works perfectly!

  2. Scotty says:

    Thankyouthankyouthankyou! I will now begin regrowing all the hair I’ve been pulling out for 2 days. Beautifully conceived solution.

  3. Steven says:

    I had been stuck on this all day and your solution works perfectly! Thank you!

  4. Bill says:

    I’m a newbie and don’t really know how this work but it does. Since an ordered set seems like a common thing to use why is it not bindings compliant?

    Thanks so much!
    BT.

  5. Rick says:

    Although this works as way of making bindings to NSTableView datasource (and clever it is), it comes up a bit short if you try to use the controller’s actions, e.g. add:. At some point Apple will have to do the proper integration (and perhaps fix the bug in the automagically generated addObject: method).

    I also found your example for the drag and drop a useful addition to the apple examples. Thanks.

  6. Matthew says:

    LOVE IT!

  7. Sulemamn says:

    Thank you .. this was driving me nuts!! Thank you for Ordered Set Array Value Transformer :)

  8. Motti Shneor says:

    Thank you so much, you helped me out of a problem I was struggling with for days.

    Now if indeed you dug deep into this matter, I’d like to ask a few further questions about your solution.

    1. When you set up an NSArrayController to work with an “Entity Name” as opposed to working with a “Class”, you usually refrain from directly binding either the content-set or the content-array of the controller, and instead have a “fetch request” or nothing at all, to have the NSArrayController read content from CoreData store. As far as I can understand, the moment you set up binding of the contentArray (or contentSet — the Entity name will be unused further. So why set it on the first place, and why you need it for the drag’n drop implementation ???

    2. Any guess why apple didn’t introduce a new “contentOrderedSet” bindable attribute to NSArrayController, in addition to the existing contentSet and contentArray? This is really weird, and NSOrderedSet is being used quite extensively in many places.

    3. Do you know how manual re-ordering of the rows lives together with sorting by columns? I need some hybrid, where the top 5 rows are manually ordered (by the user dragging there rows) and the rest are sorted by the current selected column.

    Thanks again!

Leave a Reply