Arrow keys with NSTableView - objective-c

Is it possible to navigate an NSTableView's editable cell around the NSTableView using arrow keys and enter/tab? For example, I want to make it feel more like a spreadsheet.
The users of this application are expected to edit quite a lot of cells (but not all of them), and I think it would be easier to do so if they didn't have to double-click on each cell.

In Sequel Pro we used a different (and in my eyes simpler) method: We implemented control:textView:doCommandBySelector: in the delegate of the TableView. This method is hard to find -- it can be found in the NSControlTextEditingDelegate Protocol Reference. (Remember that NSTableView is a subclass of NSControl)
Long story short, here's what we came up with (we didn't override left/right arrow keys, as those are used to navigate within the cell. We use Tab to go left/right)
Please note that this is just a snippet from the Sequel Pro source code, and does not work as is
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
{
NSUInteger row, column;
row = [tableView editedRow];
column = [tableView editedColumn];
// Trap down arrow key
if ( [textView methodForSelector:command] == [textView methodForSelector:#selector(moveDown:)] )
{
NSUInteger newRow = row+1;
if (newRow>=numRows) return TRUE; //check if we're already at the end of the list
if (column>= numColumns) return TRUE; //the column count could change
[tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
[tableContentView editColumn:column row:newRow withEvent:nil select:YES];
return TRUE;
}
// Trap up arrow key
else if ( [textView methodForSelector:command] == [textView methodForSelector:#selector(moveUp:)] )
{
if (row==0) return TRUE; //already at the beginning of the list
NSUInteger newRow = row-1;
if (newRow>=numRows) return TRUE;
if (column>= numColumns) return TRUE;
[tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
[tableContentView editColumn:column row:newRow withEvent:nil select:YES];
return TRUE;
}

Well it isn't easy but I managed to do it without having to use RRSpreadSheet or even another control. Here's what you have to do:
Create a subclass of NSTextView, this will be the field editor. For this example the name MyFieldEditorClass will be used and myFieldEditor will refer to an instance of this class.
Add a method to MyFieldEditorClass called "- (void) setLastKnownColumn:(unsigned)aCol andRow:(unsigned) aRow" or something similar, and have it save both the input parameter values somewhere.
Add another method called "setTableView:" and have it save the NSTableView object somewhere, or unless there is another way to get the NSTableView object from the field editor, use that.
Add another method called - (void) keyDown:(NSEvent *) event. This is actually overriding the NSResponder's keyDown:. The source code should be (be aware that StackOverflow's MarkDown is changing < and > to < and >):
- (void) keyDown:(NSEvent *) event
{
unsigned newRow = row, newCol = column;
switch ([event keyCode])
{
case 126: // Up
if (row)
newRow = row - 1;
break;
case 125: // Down
if (row < [theTable numberOfRows] - 1)
newRow = row + 1;
break;
case 123: // Left
if (column > 1)
newCol = column - 1;
break;
case 124: // Right
if (column < [theTable numberOfColumns] - 1)
newCol = column + 1;
break;
default:
[super keyDown:event];
return;
}
[theTable selectRow:newRow byExtendingSelection:NO];
[theTable editColumn:newCol row:newRow withEvent:nil select:YES];
row = newRow;
column = newCol;
}
Give the NSTableView in your nib a delegate, and in the delegate add the method:
- (BOOL) tableView:(NSTableView *)aTableView shouldEditColumn:(NSTableColumn *) aCol row:aRow
{
if ([aTableView isEqual:TheTableViewYouWantToChangeBehaviour])
[myFieldEditor setLastKnownColumn:[[aTableView tableColumns] indexOfObject:aCol] andRow:aRow];
return YES;
}
Finally, give the Table View's main window a delegate and add the method:
- (id) windowWillReturnFieldEditor:(NSWindow *) aWindow toObject:(id) anObject
{
if ([anObject isEqual:TheTableViewYouWantToChangeBehaviour])
{
if (!myFieldEditor)
{
myFieldEditor = [[MyFieldEditorClass alloc] init];
[myFieldEditor setTableView:anObject];
}
return myFieldEditor;
}
else
{
return nil;
}
}
Run the program and give it a go!

Rather than forcing NSTableView to do something it wasn't designed for, you may want to look at using something designed for this purpose. I've got an open source spreadsheet control which may do what you need, or you may at least be able to extend it to do what you need: MBTableGrid

I wanted to reply to the answers here but the reply button seems to be missing so I'm forced to proved an answer when I really just want to ask a question about the replies.
Anyway, I've seen a few answers for overriding the -keyDown event of the table view that say to subclass the TableView but according to every Objective-C book I've read so far, and several Apple training videos, you should very rarely if ever subclass one of the core classes. In fact every single one of them makes the point that C programmers have a fascination with subclassing and that's not how Objective-C works; that Objective-C is all about helpers and delegates not subclassing.
So, should I just ignore any of the responses that say to subclass as this seems to be in direct contradiction to the precepts of Objective-C?
--- Edit ---
I found something that worked without subclassing the NSTableView. While I do move the inheritance up one notch on the chain from NSObject to NSResponder I'm not totally subclassing the NSTableView. I'm just adding the ability to override the keyDown event.
I made the class I was using as a delegate inherit from NSResponder instead of NSObject and set the nextResponder to that class in awakeFromNib. I was then able to trap key presses using the keydown event. I of course connected the IBOutlet and set the delegate in Interface Builder.
Here's my code with the minimum needed to show the trapping of the key:
Header file
// AppController.h
#import <Cocoa/Cocoa.h>
#interface AppController : NSResponder {
IBOutlet NSTableView *toDoListView;
NSMutableArray *toDoArray;
}
-(int)numberOfRowsInTableView:(NSTableView *)aTableView;
-(id)tableView:(NSTableView *)tableView
objectValueForTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex;
#end
Here's the m file.
// AppController.m
#import "AppController.h"
#implementation AppController
-(id)init
{
[super init];
toDoArray = [[NSMutableArray alloc] init];
return self;
}
-(void)dealloc
{
[toDoArray release];
toDoArray = nil;
[super dealloc];
}
-(void)awakeFromNib
{
[toDoListView setNextResponder:self];
}
-(int)numberOfRowsInTableView:(NSTableView *)aTableView
{
return [toDoArray count];
}
-(id)tableView:(NSTableView *)tableView
objectValueForTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex
{
NSString *value = [toDoArray objectAtIndex:rowIndex];
return value;
}
- (void)keyDown:(NSEvent *)theEvent
{
//NSLog(#"key pressed: %#", theEvent);
if (theEvent.keyCode == 51 || theEvent.keyCode == 117)
{
[toDoArray removeObjectAtIndex:[toDoListView selectedRow]];
[toDoListView reloadData];
}
}
#end

Related

Cocoa Bindings - NSTableView - Swapping Values

Is an NSValueTransform subclass a good choice for displaying Core Data attributes into UI views displaying:
A number string like (0,1,2,3,etc) into a string such as (Pending, Completed, Frozen, In progress, etc)
A number string like (0,1) into a app-based image (red.png if 0, green.png if 1)
Here's what Core Data displays for the two attributes, timer and status:
Here is what I want to be displayed instead, without changing the values in Core Data:
If not to use NSValueTransformer, in what other way is this possible?
I do not want to see the data permanently converted, only for the benefit of less data stored in Core Data and better UI view items.
I have also tried to modify the attributes in the managed object class (with out KVO notification) with no luck.
Yes, NSValueTransformer subclasses work just fine for this purpose.
You can also add read-only computed properties to your managed object class, and that should work, too. Those properties can even be added by a category in the controller code, if they don't make sense as part of the model code.
For example:
+ (NSSet*) keyPathsForValuesAffectingStatusDisplayName
{
return [NSSet setWithObject:#"status"];
}
- (NSString*) statusDisplayName
{
NSString* status = self.status;
if ([status isEqualToString:#"0"])
return #"Pending";
else if ([status isEqualToString:#"1"])
return #"Completed";
// ...
}
The +keyPathsForValuesAffectingStatusDisplayName method lets KVO and bindings know that when status changes, so does this new statusDisplayName property. See the docs for +keyPathsForValuesAffectingValueForKey: to learn how that works.
I ended up using what at first appeared to be blocking the display of different info in those cells, using:
#pragma mark - Table View Delegate
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
/* tableColumn = (string) #"AutomaticTableColumnIdentifier.0"
row = (int) 0 */
NSString *identifier = [tableColumn identifier];
NSTableCellView *cellView = [tableView makeViewWithIdentifier:identifier owner:self];
NSManagedObject *item = [self.itemArrayController.arrangedObjects objectAtIndex:row];
if ([identifier isEqualToString:#"AutomaticTableColumnIdentifier.0"]) {
/* subviews returns array with 0 = Image View &
1 = Text Field */
/* First, set the correct timer image */
... logic ...
NSImageView *theImage = (NSImageView *)[[cellView subviews] objectAtIndex:0];
theImage.image = [NSImage imageNamed:#"green.gif"];
/* Second, display the desired status */
NSTextField *theTextField = (NSTextField *)[[result subviews] objectAtIndex:1];
... logic ...
theTextField.stringValue = #"Pending";
}
return cellView;
}
Apple's documentation states (somewhere) that bindings with an Array Controller can work in combination with manually populating the table view cells. It seems best and easiest to start with bindings and then refine display values manually.

Subclassing NSPopUpButton to add a bindable property

I'm trying to add a bindable property to a custom NSPopUpButton subclass.
I've created a "selectedKey" property, which is meant to store a NSString associated with selected menu item.
In control init, I set self as button target and an action for the button (valueChanged:), which in turn sets "selectedKey" in accordance with user selection:
#interface MyPopUpButton : NSPopUpButton {
NSMutableDictionary *_items;
NSString *_selectedKey;
}
#property(nonatomic, readwrite, copy) NSString* selectedKey;
- (void)addItemWithTitle:(NSString *)title andKey:(NSString *)key;
#end
#implementation MyPopUpButton
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
_items = [NSMutableDictionary new];
[NSObject exposeBinding:#"selectedKey"];
[super setTarget:self];
[super setAction:#selector(valueChanged:)];
}
return self;
}
- (void)addItemWithTitle:(NSString *)title andKey:(NSString *)key {
[super addItemWithTitle:title];
[_items setValue:title forKey:key];
}
- (void)valueChanged:(id)sender {
for (NSString *aKey in [_items allKeys]) {
if ([[_items valueForKey:aKey] isEqualToString:[self titleOfSelectedItem]]) {
self.selectedKey = aKey;
}
}
}
- (void)setSelectedKey:(NSString *)selectedKey {
[self willChangeValueForKey:#"selectedKey"];
_selectedKey = selectedKey;
[self didChangeValueForKey:#"selectedKey"];
[self selectItemWithTitle:[_items valueForKey:selectedKey]];
}
#end
This seems to work as expected: "selectedKey" property is changed when user changes PopUpButton selection.
Unfortunately, trying to bind this property, doesn't work.
[selectButton bind:#"selectedKey" toObject:savingDictionary withKeyPath:key options:#{NSContinuouslyUpdatesValueBindingOption : #YES }]
When selection is changed bind object is not updated accordingly.
What am I doing wrong?
I've created a "selectedKey" property, which is meant to store an NSString associated with selected menu item.
Bindings is definitely the way to go here, but your use of bind:toObject:withKeyPath:options is incorrect.
The value that you pass to the first argument must be one of the predefined values made available by Apple for that particular control. For NSPopUpButton objects, the available values are documented in the NSPopUpButton Bindings Reference. When you look through this document you'll see that there is no selectedKey option. There is however a selectedValue which has the following description:
An NSString that specifies the title of the selected item in the NSPopUpButton.
Thus the correct way to set up the binding is as follows:
[self.btn bind:#"selectedValue"
toObject:self
withKeyPath:#"mySelectedString"
options:nil];
This is all you need to do: when the action selector is fired the property stored at the keyPath you passed in as the third argument will already have been updated. This means that you can (i) get rid of the setSelectedKey method entirely, (ii) remove exposeBinding line, and (iii) remove the code within valueChanged: - Cocoa has already done this bit.
The example below implements just two methods, but, if I've understood your intentions, they should be all you need:
- (void)awakeFromNib {
self.btn.target = self;
self.btn.action = #selector(popUpActivity:);
[self.btn bind:#"selectedValue"
toObject:self
withKeyPath:#"mySelectedString"
options:nil];
// I've added a couple of additional bindings here; they're
// not required, but I thought they'd be instructive.
[self.btn bind:#"content"
toObject:self
withKeyPath:#"myItems"
options:nil];
[self.btn bind:#"selectedIndex"
toObject:self
withKeyPath:#"mySelectedIndex"
options:nil];
// Now that you've set the bindings up, use them!
self.myItems = #[#"Snow", #"Falling", #"On", #"Cedars"];
self.mySelectedIndex = #3; // "Cedars" will be selected on startup
// no need to set value of mySelectedString, because it will be
// updated automatically by the selectedIndex binding.
NSLog("%#", self.mySelectedString) // -> "Cedars"
}
- (void)popUpActivity:(id)sender {
NSLog(#"value of <selectedIndex> -> %#", self.mySelectedIndex);
NSLog(#"value of <selectedString> -> %#", self.mySelectedString);
}
A final point worth making is that none of the above should be a part of an NSPopUpButton subclass. It looks like you can - and therefore should - do everything you need to do without a custom subclass of this control. In my demo-app the code above belongs to the ViewController class, you should try doing this also.

How can I insert text into UITextField at the current cursor position?

I'm trying to use the UITextField's "return" key to insert a custom character. Here's what my UITextFieldDelegate method looks like:
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField insertText:#"¶"];
return NO;
}
Unfortunately, this only works some of the time:
"one two|" --> move cursor --> "one| two" --> return --> "one¶| two" (OK)
"onetwo|" --> return --> "onetwo¶|" (OK)
"onetwo|" --> move cursor --> "one|two" --> return --> "onetwo¶|" (FAIL)
In the last case I would have expected "one¶|two".
How do I ensure that the inserted text is always inserted at the cursor position?
Thanks.
The problem is when you tap the return key on the keyboard, the text field sets the selected range (the cursor position) to the end of its text before it sends you the textFieldShouldReturn: message.
You need to keep track of the cursor position so you can restore it to its prior position. Let's say you have a reference to the text field in a property:
#interface ViewController () <UITextFieldDelegate>
#property (strong, nonatomic) IBOutlet UITextField *textField;
#end
You'll need an instance variable to hold the prior selected text range (from before the return key was tapped):
#implementation ViewController {
UITextRange *priorSelectedTextRange_;
}
Then you can write a method that saves the selected text range to the instance variable:
- (void)saveTextFieldSelectedTextRange {
priorSelectedTextRange_ = self.textField.selectedTextRange;
}
and in textFieldShouldReturn:, before you insert the pilcrow, you can change the selected text range back to its prior value:
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
textField.selectedTextRange = priorSelectedTextRange_;
[textField insertText:#"¶"];
return NO;
}
But how can we make the system send the saveTextFieldSelectedTextRange message when we need it to?
The UITextFieldDelegate protocol doesn't have messages for changes to the selected range.
UITextField doesn't post any notifications for changes to the selected range.
The UITextInputDelegate protocol does have selectionWillChange: and selectionDidChange: messages, but the system sets the text field's inputDelegate to its own UIKeyboardImpl object when the text field begins editing, so we can't use the inputDelegate.
Key-value observing on the text field's selectedTextRange property isn't reliable. In my testing on the iOS 6.0 simulator, I don't get a KVO message when I move the cursor from the middle to the end of the text by tapping the text field.
The only way I can think of to reliably track changes to the text field's selected range is by adding an observer to the run loop. On every pass through the event loop, the observer runs before event processing, so it can grab the current selected range before it changes.
So we actually need another instance variable, to hold the reference to our run loop observer:
#implementation ViewController {
UITextRange *priorSelectedTextRange_;
CFRunLoopObserverRef runLoopObserver_;
}
We create the observer in viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
[self createRunLoopObserver];
}
and we destroy it in both viewDidUnload and in dealloc:
- (void)viewDidUnload {
[super viewDidUnload];
[self destroyRunLoopObserver];
}
- (void)dealloc {
[self destroyRunLoopObserver];
}
To create the observer, we need a plain old C function for it to call. Here's that function:
static void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
__unsafe_unretained ViewController *self = (__bridge ViewController *)info;
[self saveTextFieldSelectedTextRange];
}
Now we can actually create the observer and register it with the main run loop:
- (void)createRunLoopObserver {
runLoopObserver_ = CFRunLoopObserverCreate(NULL, kCFRunLoopAfterWaiting, YES, 0, &runLoopObserverCallback, &(CFRunLoopObserverContext){
.version = 0,
.info = (__bridge void *)self,
.retain = CFRetain,
.release = CFRelease,
.copyDescription = CFCopyDescription
});
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver_, kCFRunLoopCommonModes);
}
and here's how we actually deregister the observer and destroy it:
- (void)destroyRunLoopObserver {
if (runLoopObserver_) {
CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver_, kCFRunLoopCommonModes);
CFRelease(runLoopObserver_);
runLoopObserver_ = NULL;
}
}
This approach works in my testing on the iOS 6.0 simulator.
What's happening here is that you're not keeping track of the insertion point, also known as the selection range.
And to do that, you need to get somewhat deeper into the guts of what UITextField can do.
Using UITextInput (accessible as a protocol that UITextField uses), you can fetch the "selectedTextRange" property which tells you where the caret (cursor, insertion point) is and that's where you should insert your special character. This should work if you set your object to be a delegate that conforms to the "UITextInput" protocol.

primitive accessors in this example

Could someone help me understand the primitive accessors with this example : i don't understand what is automatically set and the order of those methods :
1.after a person is created, is willSave the first method called? (i guess so, because save: is called after we create a person with insertNewObjectForEntityForName )
2.in RootViewController (the second chunk of code), we then call the getter of eyeColor with : person.eyeColor :
a) in eyeColor, we call : [self eyeColorData] ,
b) but setPrimitiveEyeColorData is in willSave, which is accessible only if primitiveEyeColor exists,
c) but setPrimitiveEyeColor is in eyeColor and only called if [self eyeColorData] exists. So, i'm a bit confused with this code, could someone help me?
here's the code about eyeColor and eyeColorData :
#dynamic eyeColorData;
#dynamic eyeColor;
#interface AWPerson (PrimitiveAccessors)
- (UIColor *)primitiveEyeColor;
- (void)setPrimitiveEyeColor:(UIColor *)value;
- (NSData *)primitiveEyeColorData;
- (void)setPrimitiveEyeColorData:(NSData *)value;
#end
+ (id)personInManagedObjectContext:(NSManagedObjectContext *)moc {
return [NSEntityDescription
insertNewObjectForEntityForName:#"Person"
inManagedObjectContext:moc];
}
+ (id)randomPersonInManagedObjectContext:(NSManagedObjectContext *)moc {
AWPerson *randomPerson = [self personInManagedObjectContext:moc];
//...
randomPerson.eyeColor = [self randomColor]; //setter eyeColor
return randomPerson;
}
+ (UIColor *)randomColor {
static NSArray *colorsArray = nil;
if( !colorsArray ) {
colorsArray = [[NSArray alloc] initWithObjects:
[UIColor lightGrayColor],
[UIColor blueColor],
[UIColor greenColor], nil];
}
int randomIndex = arc4random() % [colorsArray count];
return [colorsArray objectAtIndex:randomIndex];
}
- (void)willSave {
UIColor *color = [self primitiveEyeColor];
if( color ) {
[self setPrimitiveEyeColorData:
[NSKeyedArchiver archivedDataWithRootObject:color]];
} else {
[self setPrimitiveEyeColorData:nil];
}
[super willSave];
}
- (UIColor *)eyeColor {
[self willAccessValueForKey:#"eyeColor"];
UIColor *tmpValue = [self primitiveEyeColor];
[self didAccessValueForKey:#"eyeColor"];
if( tmpValue ) return tmpValue;
NSData *colorData = [self eyeColorData];
if( !colorData ) return nil;
tmpValue = [NSKeyedUnarchiver unarchiveObjectWithData:colorData];
[self setPrimitiveEyeColor:tmpValue];
return tmpValue;
}
in RootViewController :
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
AWPerson *person = [[self fetchedResultsController] objectAtIndexPath:indexPath];
[cell setBackgroundColor:person.eyeColor];
}
Thanks
EDIT - Added info on willSave
To answer your first question, willSave is called whenever the object is saved (using the save method). So the first method called will be one of the class methods (used to create the object) or init and then, since you said that the object is saved just after it is created, willSave gets called.
I think the key to understanding this is to realize that eyeColor, primitiveEyeColor, and their setters are all ultimately interacting with the same variable in memory (the iVar named eyeColor). The difference is whether or not the code in the setter/getter (in this case the - (UIColor *)eyeColor { function) is called.
There are just a few different ways to interact with it:
[self primitiveEyeColor]; - This reads the value of the iVar directly.
[self setPrimitiveEyeColor:tmpValue]; - This sets the value of the iVar directly.
[self eyeColor] - This calls the - (UIColor *)eyeColor method in your class (which should ultimately retrieve the iVar or a representation of it).
[self setEyeColor:value] - This calls the - (void)setEyeColor:(UIColor *)newColor method in your class. Note that in this case it doesn't exist so it simply calls the primitive method (and does the KVO magic).
In this particular code, they are using a "non-standard persistent attribute" because NSManagedObject does not support UIColor's. Read about it here.
EDIT 2
To answer your other questions:
a) The color in randomPerson.eyeColor = [self randomColor] is
accessible with [self primitiveEyeColor] (in willSave)?
Yes, once eyeColor is set (either via the setEyeColor method or the setPrimitiveEyeColor method), you can read it from primitiveEyeColor and it will return the same value.
Note that once it is set, eyeColor and primitiveEyeColor return the same value and can be called from anywhere in your class (not just willSave).
b) So if [self primitiveEyeColor] != nil : in eyeColor, the line :
if( tmpValue ) return tmpValue; should therefore always be true...
when can we unarchive eyeColorData if UIColor *tmpValue = [self
primitiveEyeColor] is always returned in -(UIColor *)eyeColor?
This method only looks at eyeColorData (which was stored during the last call to willSave) if eyeColor is nil. This is an optimization because we could skip all of this and just unarchive eyeColorData every time if we wanted to. In this case, once a value is unarchived or set to a new value, it always stores that value and returns it so that we don't have to call unarchive again.
Also, there is really what I believe to be an error here (although it could be by design). Let's say that we perform the following steps:
Set eyeColor to a random color (let's say blue).
save the object.
Set eyeColor to nil
Now, if you check the color using [self eyeColor] it will see that primitiveEyeColor is nil and unarchive eyeColorData again, therefore returning the blue color that was stored previously. You should probably be over-riding the set function so that it sets eyeColorData to nil when eyeColor is set to nil. That way, checking the value of eyeColor after setting it to nil will return nil as expected.

NSPasteboard type for NSManagedObject

I need to drag a reference to an NSManagedObject between two table views of my application. What's the preferred NSPasteboard type to store a reference to an NSManagedObject?
My current solution is to store the URIRepresentation of the object's NSManagedObjectID in a NSPasteboardTypeString. I suspect there's a more elegant solution out there.
There is no standard type for all model objects since your model objects and how they're handled are unique to your application. If there was one pasteboard type for all then there'd be no telling them apart. Your own custom object should have its own drag type.
Just use a string that makes sense (maybe a #define so you can find it with auto-complete in Xcode) like "MyObjectPboardType" that resolves to "com.yourcompany.yourapp.yourobjecttype".
Use NSPasteboard's -declareTypes:owner: to declare your new type, then use -setString:forType: or one of the other -set?:forType: methods to set the information for your object's type. In your case, the use of the object ID is a perfectly acceptable identifier. Just remember managed objects' object IDs change when they're new versus persisted.
If you are dragging within tables in the same application you might as well put in pasteboard the rowIndexes (indexPaths in case you are dragging from an outlineView) of the objects in the tableView (outlineView). This might as well spare you from some unneeded CoreData access if the dataSource of the tableViews are NSArrayController (NSTreeController for outlineView).
You can then easily retrieve the dragged objects when accepting the drop since the ‘info‘ object passed to both methods ‘tableView:validateDrop:proposedRow: proposedDropOperation:‘ and to ‘tableView:acceptDrop:row:dropOperation:‘ will have a reference to the tableView originating the drag under ‘draggingSource‘ key path.
Here's a simple implementation:
extern NSString *const kMyLocalDragType = #"com.whatever.localDragType";
#implementation MyArrayControllerDataSource
.
.
.
#pragma mark - NSTableViewDataSource (Drag & Drop)
+ (NSArray *)dragTypes {
// convenience method returning all class's supported dragTypes
return #[kMyLocalDragType];
}
- (BOOL)tableView:(NSTableView *)tableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard {
[pboard declareTypes:[[self class] dragTypes] owner:self];
for (NSString *aDragType in [[self class] dragTypes]) {
if (aDragType == kMyLocalDragType) {
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes]; // we are supporting drag&drop of multiple items selected
[pboard setData:data forType:aDragType];
}
.
. // logic for other dragTypes
.
}
return YES;
}
- (NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id<NSDraggingInfo>)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)dropOperation {
NSArray *dragTypes = [info draggingPasteboard] types];
for (id aDragType in dragTypes) {
if (aDragType == kMyLocalDragType) {
return NSDragOperationCopy;
}
}
.
.// Other logic for accepting drops/affect drop operation
.
}
- (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id<NSDraggingInfo>)info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)dropOperation {
if ([info draggingPasteboard] types] containsObject:kMyLocalDragType]) {
// Retrieve the index set from the pasteboard:
NSData *data = [[info draggingPasteboard] dataForType:kMyLocalDragType];
NSIndexSet *rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:data];
NSArray *droppedObjects = [self retrieveFromTableView:tableView objectsAtRows:rowIndexes];
// droppedObjects contains dragged and dropped objects, do what you
// need to do with them, then add them to this dataSource:
[self.content insertObjects:droppedObjects];
[tableView reloadData];
[tableView deselectAll:nil];
return YES;
}
.
. // other logic for accepting drops of other dragTypes supported.
.
}
#pragma mark - Helpers
- (NSArray <NSManagedObject *> *)retrieveFromTableView:(NSTableView *)tableView objectsAtRowIndexes:(NSIndexSet *)rowIndexes {
id dataSource = [tableView dataSource];
if ([dataSource respondsToSelector:#selector(content)]) {
if ([dataSource.content respondsToSelector:#selector(objectsAtIndexes:)]) {
return [datasource content] objectsAtIndexes:rowIndexes];
}
}
return #[]; //We return an empty array in case introspection check failed
}