PDA

View Full Version : Making NSTableView cells editable (Hillegass chapter 6 challenge)




elorc
Apr 1, 2009, 11:39 AM
Hey guys, newb here. :) I've been working my way through Aaron Hillegass' "Cocoa Programming for Mac OS X" book and I decided to try his challenge in Chapter 6: Make a Data Source. The program works fine, but I decided to try adding the bonus criteria he mentioned. It says to make the rows editable and provides a hint that NSMutableArray has a method called replaceObjectAtIndex:withObject:. This has me a bit stumped because I'm not sure how to incorporate this into the existing application. The NSTableView is set to editable, so if I double-click on a row I can type on it. Those changes aren't saved though since I don't have the proper code behind it.

This is how my application looks right now (without the code relating to editing the rows):

//
// AppController.m
// DataSourceChallenge
//

#import "AppController.h"


@implementation AppController

- (id)init
{
[super init];

toDoList = [[NSMutableArray alloc] init];

NSLog(@"init");
return self;
}

- (IBAction)addIt:(id)sender
{
NSString *addedString = [textField stringValue];

if ([addedString length] == 0)
{
NSLog(@"User tried to add a blank line.");
NSRunAlertPanel(@"Error", @"You cannot add a blank line to the to-do list.", @"OK", nil, nil);
return;
}

[toDoList addObject:addedString];
NSLog(@"Added the string: %@", addedString);

[textField setStringValue:@""];
[tableView reloadData];
NSLog(@"Ran reloadData.");
}

- (int)numberOfRowsInTableView:(NSTableView *)tv
{
return [toDoList count];
}

- (id)tableView:(NSTableView *)tv objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row
{
NSString *ti = [toDoList objectAtIndex:row];
return ti;
}

@end

//
// AppController.h
// DataSourceChallenge
//

#import <Cocoa/Cocoa.h>


@interface AppController : NSObject {
IBOutlet NSTextField *textField;
IBOutlet NSButton *addButton;
IBOutlet NSTableView *tableView;
NSMutableArray *toDoList;
}
- (IBAction)addIt:(id)sender;
@end

Now I was expecting to have to add another method, something like - (IBAction)editIt:(id)sender but I'm not sure what to bind this to on the NSTableView. Is that even necessary or is there something else I need to be doing? I thought maybe something like this:

- (void)tableView:(NSTableView *)tv replaceObjectAtIndex:(int)indexRow withObject:(NSString *)tempNewString
{
[toDoList replaceObjectAtIndex:indexRow withObject:tempNewString];
[tableView reloadData];
return;
}

But this has no effect. When I double-click a row and change the text, I hit enter and it goes back to what the value was originally. I know this is a bigtime newb question but I appreciate your help. :)



Eraserhead
Apr 1, 2009, 12:48 PM
You don't add a method, you use an appropriate delegate for NSTableView to call the method in NSMutableArray ;).

elorc
Apr 1, 2009, 01:15 PM
Hmmm okay. How do I set up that delegate then? I already have made the delegate connection from the NSTableView to my AppController class in Interface Builder.

I changed my init to:

- (id)init
{
[super init];

toDoList = [[NSMutableArray alloc] init];
[tableView setDelegate:self];

NSLog(@"init");
return self;
}

Then I removed the replaceObjectAtIndex that I had before with:

- (void)tableView:(NSTableView *)tv editColumn:(NSInteger)columnIndex row:(NSInteger)rowIndex withEvent:(NSEvent *)theEvent select:(BOOL)flag
{
NSString *tempString = [tableView valueAtIndex:rowIndex];
[toDoList replaceObjectAtIndex:rowIndex withObject:tempString];
[tableView reloadData];
return;
}

It still doesn't work. Same behavior: I change the field, hit enter, it reverts back to the old value. I don't understand what to try next. What am I missing/screwing up?

Eraserhead
Apr 1, 2009, 01:31 PM
[tableView setDelegate:self]; isn't needed in the init if the connection is done in Interface Builder

- (void)tableView:(NSTableView *)tv editColumn:(NSInteger)columnIndex row:(NSInteger)rowIndex withEvent:(NSEvent *)theEvent select:(BOOL)flag
{
...
}

That's not a delegate method, you want to use controlTextDidEndEditing: or something ;).

elorc
Apr 1, 2009, 02:39 PM
I'm starting to get really frustrated and annoyed with this. Here's what I'm using now:

- (void)controlTextDidEndEditing:(NSNotification *)aNotification
{
int currentRow = [tableView selectedRow];
if (currentRow == -1)
{
NSRunAlertPanel(@"Error", @"No row is selected.", @"OK", nil, nil);
return;
}

NSString *tempString = [tableView objectAtIndex:currentRow];

[toDoList replaceObjectAtIndex:currentRow withObject:tempString];
[tableView reloadData];
return;
}

When I edit a field, I now get the following in my debugging window:

"2009-04-01 15:06:42.950 DataSourceChallenge[1416:10b] *** -[NSTableView objectAtIndex:]: unrecognized selector sent to instance 0x129690
2009-04-01 15:06:42.950 DataSourceChallenge[1416:10b] *** -[NSTableView objectAtIndex:]: unrecognized selector sent to instance 0x129690"

Also, a warning shows up on the "NSString *tempString ..." line that says: "Warning: 'NSTableView may not respond to '-objectAtIndex:' (Messages without a matching method signature will be assumed to return 'id' and accept '...' as arguments.')".

I've tried objectAtIndex, objectValue, valueAtIndex, and I get the same exact result for everything. I have no clue how to get the new value from the table and the documentation I've read hasn't been helpful at all. I know the rest of the code works fine because if I replace the last three lines with:

[toDoList replaceObjectAtIndex:currentRow withObject:@"Blah"];
[tableView reloadData];
return;

the row updates to "Blah" once the focus is changed or the user presses the enter key. Why is this so complicated to do in Objective-C? What am I missing now?

Eraserhead
Apr 1, 2009, 04:21 PM
In the line:

NSString *tempString = [tableView objectAtIndex:currentRow]; tableView should be replaced with toDoList

elorc
Apr 1, 2009, 04:38 PM
In the line:

NSString *tempString = [tableView objectAtIndex:currentRow]; tableView should be replaced with toDoList

Now using:

- (void)controlTextDidEndEditing:(NSNotification *)aNotification
{
int currentRow = [tableView selectedRow];
if (currentRow == -1)
{
NSRunAlertPanel(@"Error", @"No row is selected.", @"OK", nil, nil);
return;
}

NSString *tempString = [toDoList objectAtIndex:currentRow];

[toDoList replaceObjectAtIndex:currentRow withObject:tempString];
[tableView reloadData];
return;
}

I get no warnings or errors, but it still just goes back to the old value. By setting tempString to the value of toDoList at the selected index, won't that just reassign the old value instead of the new one?

eddietr
Apr 1, 2009, 05:40 PM
elorc, I think you're very much on the wrong track here and making this harder than it actually is. For what you want to do, you don't need to implement a TableView delegate at all.

This is just a simple matter of implementing one extra function in the datasource, which is setObjectValue:forTableColumn:row. An NSTableView will call this method whenever the user edits a cell.

So here is an extremely simple rough example of a one column table (without validation, or error checking, etc) where the user can edit the string in each cell. This assumes that you create an instance of this AppController and connect it as the dataSource for your NSTableView in Interface Builder.

Of course, an even easier way to do this is using bindings. But without bindings, this is how it would work:


#import <Foundation/Foundation.h>

// An instance of AppController is connected as the dataSource
// for an NSTableView in a nib.

@interface AppController : NSObject {

NSMutableArray* ourArray;

}

@end



@implementation AppController


#pragma mark NSTableView dataSource methods

- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView {

return [ourArray count];
}

- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex {

return [ourArray objectAtIndex:rowIndex];

}

- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex {

[ourArray replaceObjectAtIndex:rowIndex withObject:anObject];

}


#pragma mark init/dealloc

- (id) init {

if (self = [super init]) {

ourArray = [[NSMutableArray arrayWithObjects: @"one", @"two", @"three", nil] retain];

}

return self;

}


- (void) dealloc {

[ourArray release];

[super dealloc];
}

@end


Hopefully that gets you back on track and then you can build from there. Good luck.

Eraserhead
Apr 1, 2009, 06:23 PM
Sorry :o I haven't use the method described above.

elorc
Apr 2, 2009, 08:52 AM
Eddietr: That's perfect. I understand what's going on and feel like an idiot for not having figured it out sooner. Thank you. :)

Eraserhead: No problem. I really appreciate the effort to help me straighten this thing out!

One thing I was curious about though, and this isn't part of the challenge in the book but kind of an additional thing I thought of... what's the best way to get the text field to add its contents to the tableview if the user presses the enter key (instead of clicking on the command button)? I was looking at textDidChange: but I'm not sure if that's the approach. If it is, how do I determine what the change was (i.e., what the last key pressed was)?

I know how to do this in C#/VB.NET but obviously they are entirely separate beasts. :)

eddietr
Apr 2, 2009, 10:17 AM
what's the best way to get the text field to add its contents to the tableview if the user presses the enter key (instead of clicking on the command button)?

The typical way (consistent with the Apple guidelines) is to keep the button, but make return '\r' the key equivalent for the button. You can also do this by making that button the default button for the window.

GregX999
Apr 8, 2009, 09:08 PM
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex {
[ourArray replaceObjectAtIndex:rowIndex withObject:anObject];
}



What a coincidence!! I've just now gotten to the same exercise in the same book and had the EXACT same problem - trying to use "ControlTextDidEndEditing" and getting the same exact errors.

After struggling and searching the net for about an hour now, I found this thread and the above method worked like a charm.

BUT... why isn't that method listed in the docs for NSTableView? :mad:

Greg

lee1210
Apr 8, 2009, 09:24 PM
I know nothing about this stuff, but it looks like that method is in the protocol NSTableDataSource:
http://developer.apple.com/documentation/Cocoa/Reference/ApplicationKit/Protocols/NSTableDataSource_Protocol/Reference/Reference.html#//apple_ref/occ/cat/NSTableDataSource

The notes in eddietr's code says that an instance of the AppController is connected to the table's datasource, not the table itself.

-Lee

eddietr
Apr 8, 2009, 10:38 PM
BUT... why isn't that method listed in the docs for NSTableView?

It's in the docs for an NSTableView dataSource.


The notes in eddietr's code says that an instance of the AppController is connected to the table's datasource, not the table itself.


Oops, what I actually meant is that in this example this AppController is the tableview's datasource, not connected to the datasource. I should have made that more clear.

pawzlion
Jan 15, 2010, 01:39 AM
I was also at the same point in the book and having trouble with this one. I knew the correct way to do the editable bit but I was having some problems with my mutable array initialisation