I'm not sure what I'm doing wrong. Since I couldn't find any other questions (or even documentation) about this, it seems do be normally working without problems for other people.
I'm simply trying to get a view based NSTableView to do support editing of it's content. I.e. the app displays a NSTableView with one column and several rows, containing a NSTextField with some content. I want to be able to (double) click on a cell and edit the cell's content. So basically the normal behavior of a cell based NSTableView where the tableView:setObjectValue:forTableColumn:row: method is implemented.
I analyzed the Complex TableView example in the TableViewPlayground sample code from Apple (which is supporting editing of cell content), but I cannot find the setting/code/switch which is enabling the editing.
Here's a simple sample project (Xcode 6.1.1, SDK 10.10, storyboard based):
Header:
#import <Cocoa/Cocoa.h>
#interface ViewController : NSViewController
#property (weak) IBOutlet NSTableView *tableView;
#end
Implementation:
#import "ViewController.h"
#implementation ViewController
{
NSMutableArray* _content;
}
- (void)viewDidLoad {
[super viewDidLoad];
_content = [NSMutableArray array];
for(NSInteger i = 0; i<10; i++) {
[_content addObject:[NSString stringWithFormat:#"Item %ld", i]];
}
}
#pragma mark - NSTableViewDataSource
- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView
{
return _content.count;
}
#pragma mark - NSTableViewDelegate
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
NSTableCellView* cell = [tableView makeViewWithIdentifier:#"CellView" owner:self];
cell.textField.stringValue = _content[row];
return cell;
}
- (IBAction)endEditingText:(id)sender {
NSInteger row = [_tableView rowForView:sender];
if (row != -1) {
_content[row] = [sender stringValue];
}
}
#end
The storyboard file looks like this:
The datasource and delegate of the table view are set to the view controller.
When running this app, the table view displays the 10 test rows, but it is not possible to edit one of the rows.
Why is that? What did I miss here?
I double checked all attributes of the NSTableView (and it's contents) to be the same as in the TableViewPlayground sample from Apple. And after several hours of searching the documentation and internet for helpful hints without any success, I'm kind of frustrated. All you can find on view based NSTableViews are non-editable samples or very vague information on editable content. And of course, there is tons of information, documentation and samples on editable, cell based NSTableViews...
A zip with my sample project is downloadable here:
TableTest.zip
Even though all the pieces for editing a view based NSTableView are present in the question and answer, I still had trouble putting it all together. The following demo is in Swift, using Xcode 6.3.2, but it should be easy to follow for the objective-C cavemen/womens. A full code listing is at the end.
Let's start here:
NSTableViewDataSource Protocol Reference
Setting Values
- tableView:setObjectValue:forTableColumn:row:
Swift:
optional func tableView(_ aTableView: NSTableView,
setObjectValue anObject: AnyObject?,
forTableColumn aTableColumn: NSTableColumn?,
row rowIndex: Int)
Objective-C:
- (void)tableView:(NSTableView *)aTableView
setObjectValue:(id)anObject
forTableColumn:(NSTableColumn *)aTableColumn
row:(NSInteger)rowIndex
Discussion: This method is intended for use with cell-based table views, it must not be used with view-based table views. In
view-based tables, use target/action to set each item in the view
cell.
If you're like me, you searched through the NSTableViewDelegate and NSTableViewDataSource protocols looking for some kind of edit method to use. However, the Discussion in the quote above is telling you that things are much simpler.
1) If you look in the document outline for your TableView in Xcode, you'll see something like this:
A cell in the TableView is represented by a Table Cell View. The cell contains a few items, and by default one of the items is an NSTextField. But where's the NSTextField in the document outline?! Well, the controls in the document outline have an icon that looks like a slider next to their names. Take a look. Inside the cell, you'll see something which has a slider icon next to it. And, if you select that line in the document outline, then open the Identity Inspector, you'll see that it's an NSTextField:
You can consider that just a regular old NSTextField.
When you implement your NSTableViewDataSource protocol methods:
import Cocoa
class MainWindowController: NSWindowController,
NSTableViewDataSource {
...
...
var items: [String] = [] //The data source: an array of String's
...
...
// MARK: NSTableViewDataSource protocol methods
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return items.count
}
func tableView(tableView: NSTableView,
objectValueForTableColumn tableColumn: NSTableColumn?,
row: Int) -> AnyObject? {
return items[row]
}
}
..the TableView takes the value returned by the second method and assigns it to a property named objectValue in the outer Table Cell View--in other words the TableView does not use the returned value to set the NSTextField (i.e. the inner Table View Cell). That means your data source items will not be displayed in the TableView because the NSTextField is what displays an item. In order to set the value of the NSTextField, you need to connect, or bind, the NSTextField's value to the objectValue property. You do that in the Bindings Inspector:
Warning: Make sure you don't check the Bind to checkbox until after you select the object you want to bind to. If you check the
checkbox first, an object will be inserted into your document outline,
and if you don't notice it, you will get errors when you run your
program. If you accidentally check the Bind to checkbox first, make
sure you delete the automatically added object in your document
outline. Xcode creates a separate section for the added object, so it is easy to spot in your document outline.
2) Backtracking for a moment, you are probably familiar with connecting a button to an action method, and thereafter if you click on the button the action method will execute. On the other hand, with an NSTextField you typically declare an IBOutlet, which you then use to get or set the NSTextField's stringValue.
However, an NSTextField can also trigger the execution of an action method. Wha??! But you can't click on an NSTextfield like you can a button! Nevertheless, an NSTextField has a trigger, just like a button click, which will cause the execution of an action method, and the trigger is: done editing the NSTextField. How does the NSTextField know when you are done editing it? There are two ways:
You hit Return.
You click on some other control.
You can choose the trigger in the Attributes Inspector:
3) As #Paul Patterson showed in his answer, the next thing you need to do is set the NSTextField's Behavior to Editable in the Attributes Inspector.
4) Then connect the NSTextField to the action method that you want to execute. If you haven't used the following trick to connect a control to an action method, you should try it some time:
Select your .xib file in the Project Navigator, so that the window and
its controls are displayed. Then click on the Assistant Editor(the
two_interlocking_rings icon at the top of the Xcode window on the far right)--that will display your Controller file(if some other file is shown, then use the jump bar to navigate to your Controller file). Then Control+drag
from the NSTextField (in the document outline) to the spot in your
Controller file where you want to create your action method:
When you release, you'll see this popup window:
If you enter the same information as shown, the following code will be entered in the file:
#IBAction func onEnterInTextField(sender: NSTextField) {
}
And...the connection between the NSTextField and the action method will already have been made. (You also can use those steps to create and connect an IBOutlet.)
5) Inside the action method, you can get the currently selected row, i.e. the one that has just been edited, from the TableView:
#IBAction func onEnterInTextField(sender: NSTextField) {
let selectedRowNumber = tableView.selectedRow //tableView is an IBOutlet connected to the NSTableView
}
Then I got stumped by how to get the text of the selected row in a TableView, and back to the docs I scrambled looking through the TableView and protocol methods. But, we all know how to get the stringValue of an NSTextField, right? The sender is the NSTextField you were editing:
#IBAction func onEnterInTextField(sender: NSTextField) {
let selectedRowNumber = tableView.selectedRow //My Controller has an IBOutlet property named tableView which is connected to the TableView
if selectedRowNumber != -1 { //-1 is returned when no row is selected in the TableView
items[selectedRowNumber] = sender.stringValue //items is the data source, which is an array of Strings to be displayed in the TableView
}
}
If you don't insert the new value in the data source, then the next time the TableView needs to display the rows, the original value will get displayed again, overwriting the edited changes. Remember, the TableView retrieves the values from the data source--not the NSTextField. Then the NSTextField displays whatever value the TableView assigned to the cell's objectValue property.
One last thing: I got a warning that said I couldn't connect the NSTextField to an action inside a class if the class wasn't a delegate of the TableView....so I connected the TableView's delegate outlet to File's Owner:
I had previously set the File's Owner to be my Controller(=MainWindowController), so after I made that connection, the MainWindowController--which contained the action method for the NSTextField--became the delegate of the TableView, and the warning went away.
Random tips:
1) I found the easiest way to start editing an NSTextField is to select a row in the TableView, then hit Return.
2) NSTableView's come with two columns by default. If you select one of the columns in the document outline, then hit Delete on your keyboard, you can make a one column table--however the TableView still shows the column divider, so it looks like there are still two columns. To get rid of the column divider, select the Bordered Scroll View - Table View in the document outline, then drag one of the corners to resize the TableView--the single column will instantly resize itself to take up all the available space.
Credit for steps #1 and #2, and Random tip #2: Cocoa Programming For OS X (5th Edition, 2015).
Full code listing:
//
// MainWindowController.swift
// ToDo
//
//import Foundation
import Cocoa
class MainWindowController: NSWindowController,
NSTableViewDataSource {
//#IBOutlet var window: NSWindow? -- inherited from NSWindowController
#IBOutlet weak var textField: NSTextField!
#IBOutlet weak var tableView: NSTableView!
var items: [String] = [] //The data source: an array of String's
override var windowNibName: String {
return "MainWindow"
}
#IBAction func onclickAddButton(sender: NSButton) {
items.append(textField.stringValue)
tableView.reloadData() //Displays the new item in the TableView
}
#IBAction func onEnterInTextField(sender: NSTextField) {
let selectedRowNumber = tableView.selectedRow
if selectedRowNumber != -1 {
items[selectedRowNumber] = sender.stringValue
}
}
// MARK: NSTableViewDataSource protocol methods
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return items.count
}
func tableView(tableView: NSTableView,
objectValueForTableColumn tableColumn: NSTableColumn?,
row: Int) -> AnyObject? {
return items[row]
}
}
The Connections Inspector showing all the connections for File's Owner (= MainWindowController):
By default, each cell (instance of NSTableCellView) has an NSTextField sitting on it. When you're editing the cell, what you're in fact editing is this text field. Interface Builder makes this text-field non-editable:
All you need to do is set the Behaviour pop-up to Editable. Now you can edit the text-field with a return hit or a single-click.
Just a correction to the accepted answer - you should get the row and column using the tableView.row(for: NSView) and tableView.column(for: NSView. Other methods may not be reliable.
#IBAction func textEdited(_ sender: Any) {
if let textField = sender as? NSTextField {
let row = self.tableView.row(for: sender as! NSView)
let col = self.tableView.column(for: sender as! NSView)
self.data[row][col] = textField.stringValue
print("\(row), \(col), \(textField.stringValue)")
print("\(data)")
}
}
I've got a view-based NSTableView, using Cocoa Bindings to change the values of some labels and images in the cell. It all works great. However, I want to add a button to the cell. I've got the button working, but its action method only has the button as sender, which means I have no idea of the content of the cell that the button is in. Somehow I need to store some extra data on the button - at the very least the row index that the button is in. I subclassed NSButton and used my subclass in the cell, but Interface Builder doesn't know about the extra property so I can't bind to it. If I wanted to bind it in code, I don't know the name of the object or keypath that would be passed to it.
How can I get this to work?
You can use rowForView in your action method to get the row value
- (IBAction)doSomething:(id)sender
{
NSInteger row = [_myTableView rowForView:sender];
}
You can use the Identity field in Interface Builder to associate a table cell view from the nib with an instance in your code:
Additionally you have to implement - tableView:viewForTableColumn:row: in your table view's delegate. (Don't forget to connect the delegate in IB)
- (NSView*)tableView:(NSTableView*)tableView viewForTableColumn:
(NSTableColumn*)tableColumn row:(NSInteger)row
{
SSWButtonTableCellView *result = [tableView makeViewWithIdentifier:#"ButtonView" owner:self];
result.button.title = [self.names objectAtIndex:row][#"name"];
result.representedObject = [self.names objectAtIndex:row];
return result;
}
I added representedObject property in my NSTableCellView subclass, which I set in the above table view delegate method.
Your custom table cell view can later use that object in it's action. e.g.:
- (IBAction)doSomething:(id)sender
{
NSLog(#"Represented Object:%#", self.representedObject);
}
I have a view based tableview with a button which displays a popover:
How would I go about setting the selected to the row that holds the pressed button.
I'm using core data, with an array controller. I plan to have the tableview in the popover show related objects.
Just add a tag equal to row to every cell button, button.tag = indexPath.row;
Then in your button's selector you just call
[self tableView:self.tableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:button.tag inSection:0]];
I have a view-based NSTableView that is populated through bindings. My textFields & imageViews are bound to the NSTableCellView's objectValue's properties.
If I want to have an edit/info button in my NSTableCellView:
Who should be the target of the button's action?
How would the target get the objectValue that is associated with the cell that the button is in?
I'd ultimately like to show a popover/sheet based on the objectValue.
I found an additional answer: The Answer above seems to assume you're using bindings on your table view. Since I'm kind of a noob I found a way to get the button inside the table view cell.
- (IBAction)getCellButton:(id)sender {
int row = [xmlTable rowForView:sender];
}
This way when you click on the button inside the row, you don't have to have the row selected. It will return the int value of the row to match up with a datasource in an array without bindings.
Your controller class can be the target. To get the object value:
- (IBAction)showPopover:(id)sender {
NSButton *button = (NSButton *)sender;
id representedObject = [(NSTableCellView *)[button superview] objectValue];
}
Or, use a subclass of NSTableCellView, make the cell view the target of the button's action, and call [self objectValue] to get the object.
I'm currently trying to use a new view-based NSOutlineView in my Cocoa app. As I'm not using bindings, so I implemented all required delegate and datasource methods in my controller.
In interface builder I've added a NSOutlineView with a highlighting set to SourceList and Content Mode set to View Based. Thus, there were two default table cell views provided (one Header cell with HeaderCell set as identifier and one data cell with DataCell set as identifier)
This is what it looks like in interface builder, header cell views correctly show a grey-blue textField while data cell views have a image view and a textField with correct color and font settings
To provide the views, I use the following code, to return a DataCell-view or a HeaderCell-view and set the textField of the cell accordingly, based on the corresponding identifier set in interface builder.
- (NSView *)outlineView:(NSOutlineView *)outlineView
viewForTableColumn:(NSTableColumn *)tableColumn
item:(id)item {
NSTableCellView *result = nil;
if ([item isKindOfClass:[NSMutableDictionary class]]) {
result = [outlineView makeViewWithIdentifier:#"HeaderCell" owner:self];
id parentObject = [outlineView parentForItem:item] ? [outlineView parentForItem:item] : groupedRoster;
[[result textField] setStringValue:[[parentObject allKeys] objectAtIndex:0]];
} else {
result = [outlineView makeViewWithIdentifier:#"DataCell" owner:self];
[item nickname] ? [[result textField] setStringValue:[item nickname]] : [[result textField] setStringValue:[[item jid] bare]];
}
return result;
}
Running everything it looks like the following.
Could anybody provide me with hints, to why the header cell is neither bold, nor correctly colored, when selected?
You need to implement the -outlineView:isGroupItem: delegate method and return YES for your header rows. That will standardize the font and replace the disclosure triangle on the left with a Show/Hide button on the right. You will still need to manually uppercase your string to get the full effect.
I'm not sure if the group row delegate method above makes the selection style look okay or not. However, you normally don't want the header rows to be selectable at all in source lists, which you by returning NO for header items from the -outlineView:shouldSelectItem: delegate method.
I have created a little sample project which includes a source list and also uses the -outlineView:isGroupItem: method as #boaz-stuller has suggested.
Display a list of items
Edit the items in a master-detail fashion
Remove and add items
Usage of bindings
Check out besi/mac-quickies on github.
Most of the stuff is either done in IB or can be found in the AppDelegate