How to get custom table cell views into NSTableView? - macos

I have a NSTableView that uses mostly standard NSTextTableCellViews but I want some other cells that contain another UI component in my table in one or two rows. The question is: Where do I define those custom cells so that Cocoa finds them?
I just dropped a custom NSTableCellView into my XIB (same XIB in that the table is) and gave it an identifier. But then using this code it will obviously not find the cell ...
func tableView(tableView:NSTableView, viewForTableColumn tableColumn:NSTableColumn?, row:Int) -> NSView?
{
var view:NSTableCellView?;
if (tableColumn?.identifier == "labelColumn")
{
view = tableView.makeViewWithIdentifier("labelCell", owner: nil) as? NSTableCellView;
view!.textField?.stringValue = _formLabels[row];
}
else if (tableColumn?.identifier == "valueColumn")
{
if (row == 1)
{
view = tableView.makeViewWithIdentifier("customCell", owner: nil) as? NSTableCellView;
println("\(view)"); // nil - Not found!
}
else
{
view = tableView.makeViewWithIdentifier("valueCell", owner: nil) as? NSTableCellView;
view!.textField?.stringValue = _formValues[row];
}
}
return view;
}
All works but the cell with id customCell will not be found. How do I tell Cocoa to find it?

You need to drop the new cell view in the table column that would contain that kind of cell view (the one whose identifier is "valueColumn", in this case).

Related

NSOutlineView - Get NSTableCellView for a Given Item

My view-based outline view displays a custom context menu (right-click menu) for its rows.
One of the menu items on the menu is "Rename...", and the menu item's representedObject property is set to the object represented by the outline view row:
let menu = NSMenu()
// ...other menu items...
let renameItem = NSMenuItem(
title: "Rename...",
action: #selector(OutlineViewController.rename(_:)),
keyEquivalent: "")
renameItem.representedObject = object
menu.addItem(renameItem)
On the action side, I want to make the text field in the table cell editable, programmatically. The problem is, I am not sure of how to get a reference to the table cell from the represented object alone.
This is my action method:
#IBAction func rename(_ sender: Any) {
guard let menuItem = sender as? NSMenuItem else { return }
guard let item = menuItem.representedObject else { return }
I can get the row for the represented object (Int):
let row = outlineView.row(forItem: item)
...and the row view (NSTableRowView):
let rowView = outlineView.rowView(atRow: row, makeIfNecessary: false)
I can get the column index (Int) and column (NSTableColumn):
let columnIndex = outlineView.column(withIdentifier: "TitleColumn")
let column = outlineView.tableColumns[columnIndex]
...and attempt to get the cell view (NSTableCellView):
guard let cell = outlineView(outlineView, viewFor: column, item: item) as? NSTableCellView else {
return
}
Finally, I try to make the text field editable:
guard let textField = cell.textField else {
return
}
textField.becomeFirstResponder()
All these test pass (I set breakpoints), but nothing happens: The text field does not become editable (unlike when double clicking).
What am I doing wrong?
Edit I have realized that when I call:
outlineView(outlineView, viewFor: column, item: item)
This is basically the NSOutlineViewDelegate method that my view controller is implementing, so instead of giving me the cell already on screen, it is creating a new copy on demand. Kind of like calling
UITableViewController.tableView(_:,cellForRowAt:)
// Data source method, creates or dequeues a new cell to pass
// back to the table for display
...instead of calling:
UITableView.cellForRowAt(_:)
// Method of table view proper; returns existing cell if
// already on screen, or defers to the data source (method
// above) otherwise
So instead of calling a delegate method that creates the cell view anew, I should query the outline view itself. However, the only meaningful method I can see is:
func rowView(atRow row: Int, makeIfNecessary: Bool) -> NSTableRowView?
...which returns an NSTableRowView...
Solved it. Just had to dig a little deeper into the API:
// Get row index
let row = outlineView.row(forItem: item)
// Get view for the whole row:
guard let rowView = outlineView.rowView(atRow: row, makeIfNecessary: false) else {
return
}
// THIS is the missing piece: Get row subview for the given column
// (= cell)
guard let cell = rowView?.view(atColumn: 0) as? NSTableCellView else {
return
}
Now I have a reference to the actual text field already on screen, and it becomes editable:
self.view.window?.makeFirstResponder(cell.textField)

Confused about NSTableView Identifier on Table Cell View

I am able to create simple view based NSTableViews but there's one point I don't understand about identifiers.
In an NSTableView you typically give a column an identifier and then implement the delegate method:
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
And then you switch on the column to do what you need, something like:
switch tableColumn!.identifier {
case "firstColumn":
//do something...
case "secondColumn":
//do something else...
default:
return nil
}
However additionally you can give each Table Cell View an identifier as well. So in the above example, say I didn't give the identifier to the column, and instead gave the identifier to the Table Cell View itself.
I presumed that then I could do something like this in the delegate method:
if let firstColumnCellView = tableView.make(withIdentifier: "firstColumnCell", owner: self) as? NSTableCellView {
view.textField?.stringValue = "Hi! I'm in the first column"
return view
} else if let secondColumnCellView = tableView.make(withIdentifier: "secondColumnCell", owner: self) as? NSTableCellView {
view.textField?.stringValue = "Hi! I'm in the second column"
return view
} else {
return nil
}
This works, but never makes it past the first if let statement, and so all my cells say "Hi! I'm in the first column"
More Info:
Something else I don't understand: it seems that the Table Cell View identifier overrides the identifier to the column.
If I go to the document outline and assign identifiers something like this:
tableColumn: "firstColumn"
tableViewCell: "firstColumnCell"
tableColumn: "secondColumn"
tableViewCell: "secondColumnCell"
and then supply both the column identifier and the cell identifier, it works!
switch tableColumn!.identifier {
case "firstColumn":
if let firstColumnCellView = tableView.make(withIdentifier: "firstColumnCell", owner: self) as? NSTableCellView {
view.textField?.stringValue = "Hi! I'm in the first column"
return view
} else {
return nil
}
case "secondColumn":
if let secondColumnCellView = tableView.make(withIdentifier: "secondColumnCell", owner: self) as? NSTableCellView {
view.textField?.stringValue = "Hi! I'm in the second column"
return view
} else {
return nil
}
default:
return nil
}
But it crashes if I allow the switch statement to ignore the cell identifier for the second column, and fall through to trying to use the column identifier.
switch tableColumn!.identifier {
case "firstColumn":
if let firstColumnCellView = tableView.make(withIdentifier: "firstColumnCell", owner: self) as? NSTableCellView {
view.textField?.stringValue = "Hi! I'm in the first column"
return view
} else {
return nil
}
default:
break
}
let cellView = tableView.make(withIdentifier: tableColumn!.identifier, owner: self) as! NSTableCellView
cellView.textField?.stringValue = "hello"
return cellView
//CRASH: Unexpectedly found nil when unwrapping tableColumn!.identifier
// The column both exists and has an identifier of "secondColumn", so how could
//this be nil?
And it seems I can confirm this overriding behavior by renaming the secondColumnCell to the same name as the secondColumn:
tableColumn: "firstColumn"
tableViewCell: "firstColumnCell"
tableColumn: "secondColumn" <--- Same Name
tableViewCell: "secondColumn" <-- Same Name
And now the code runs as expected and doesn't crash.
If I read your last chunk of code correctly, you instantiate (or retrieve from a used-views-no-longer-on-screen pool - see here) a view with the identifier firstColumnCell.
As long as the identifier is valid (you have somewhere a nib defining the view) the method will always return a non-nil view so the first if let ... will always succeed.
So the view.textField?.stringValue = "Hi! I'm in the first column" will execute thus showing the message in the cell and then it will return the view to be used by the NSTableView and exit your method.
The next if let ... statements will never have a chance to execute.

Select and return selection of multiple rows in a single column Swift OSX table [duplicate]

I have a NSTableView with one column. I would like to print the row number of the row that the user has clicked on. I am not sure where I should start with this. Is there a method for this?
You can use the selectedRowIndexes property from the tableView in the tableViewSelectionDidChange method in your NSTableView delegate.
In this example, the tableView allows multiple selection.
Swift 3
func tableViewSelectionDidChange(_ notification: Notification) {
if let myTable = notification.object as? NSTableView {
// we create an [Int] array from the index set
let selected = myTable.selectedRowIndexes.map { Int($0) }
print(selected)
}
}
Swift 2
func tableViewSelectionDidChange(notification: NSNotification) {
var mySelectedRows = [Int]()
let myTableViewFromNotification = notification.object as! NSTableView
let indexes = myTableViewFromNotification.selectedRowIndexes
// we iterate over the indexes using `.indexGreaterThanIndex`
var index = indexes.firstIndex
while index != NSNotFound {
mySelectedRows.append(index)
index = indexes.indexGreaterThanIndex(index)
}
print(mySelectedRows)
}
Use -selectedRowIndexes
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSTableView_Class/#//apple_ref/occ/instp/NSTableView/selectedRowIndexes
Then you can use those indexes to grab the data from your dataSource
(typically an array)

Edit data in the model according to the user's input in view-based NSTableView

I have a view-based NSTableView where all the cells are editable. I need to refresh the data from the model every time the user modifies a textField from the view.
All the doc I find is related to the cell-based NSTableView.
Does anyone have a clue about this?
EDIT:
I'm using data source to populate this NSTableView.
This is the code of the Controller of the NSTableView
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
#IBOutlet var globalView: NSView!
#IBOutlet var songsTableView: NSTableView!
var tableContents = NSMutableArray()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
for (song) in songManager.songs {
var obj = Dictionary<String,String>()
obj["title"] = song.title
obj["artist"] = song.artist
tableContents.addObject(obj)
}
songsTableView.reloadData()
}
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return tableContents.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView?{
var obj = tableContents[row] as Dictionary<String,String>
let column = tableColumn?.identifier
var cellView = tableView.makeViewWithIdentifier(column!, owner: self) as NSTableCellView
if column == "title" {
cellView.textField?.stringValue = obj["title"]!
}
if column == "artist" {
cellView.textField?.stringValue = obj["artist"]!
}
cellView.textField?.editable = true
return cellView
}
}
And this is the code of the class that manages the data.
var songManager = SongManager()
struct song {
var title = "No name"
var artist = "No artist"
}
class SongManager: NSObject {
var songs = [song]()
func addSong(title: String, artist: String) {
songs.append(song(title: title, artist: artist))
}
}
I have not touched the row that the storyboard creates by default, so I guess it contains a single NSTextField.
I get to display the data, but cannot detect when the user tried to modify a textfield.
Given how things are currently set up, the simplest approach is probably to connect the text field's action selector to an action method on a target, such as your controller. You can do that in IB or in your tableView(_:viewForTableColumn:row:) method.
In that action method, you can call songsTableView.rowForView(sender) to determine which row was edited. Each column's text field would have a different action method, such as changeTitle() or changeArtist(), so that you know which column was edited. (You could also use songsTableView.columnForView(sender), then get the table column by using the index in songsTableView.tableColumns[col], and checking the returned column's identifier. For that, you would assign specific identifiers to the columns rather than letting IB assign them automatically.)
Once you have the row, you look up your dictionary using var obj = tableContents[row] as Dictionary<String,String> and set the value for the key appropriate to the action method (or column identifier) to the sender's stringValue.

Setting NSTableView cell text

I have a NSTableView that I want to populate with 20 cells, each of them will say "Test". I'm fluent with UITableView's but not so much with NSTableViews, so I went hunting online to figure this out. Oh, how confused that made me! I understand that I need to use the numberOfRowsInTableView function, but how do I set the text of the cell? Each source I find seems to do everything in a different way. For instance, this site uses:
func tableView(tableView: NSTableView!, viewForTableColumn tableColumn: NSTableColumn!, row: Int) -> NSView! {
// 1
var cellView: NSTableCellView = tableView.makeViewWithIdentifier(tableColumn.identifier, owner: self) as NSTableCellView
// 2
if tableColumn.identifier == "BugColumn" {
// 3
let bugDoc = self.bugs[row]
cellView.imageView!.image = bugDoc.thumbImage
cellView.textField!.stringValue = bugDoc.data.title
return cellView
}
return cellView
}
I tried that but I got an error - the code found nil while unwrapping an optional. Then I tried what I found here:
func tableView(tableView: NSTableView, dataCellForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSCell? {
if tableColumn == nil { return nil }
if tableColumn!.identifier != "right" { return nil }
let cell = NSPopUpButtonCell()
cell.bordered = false
cell.menu!.addItemWithTitle("one", action: nil, keyEquivalent: "")
cell.menu!.addItemWithTitle("two", action: nil, keyEquivalent: "")
cell.menu!.addItemWithTitle("three", action: nil, keyEquivalent: "")
cell.selectItemAtIndex(1) // <--- obviously ignored ?!
return cell
}
So my question is, how do I set the cell text? How do the two examples I've inserted above differ in what they do? Please, make some sense of this - cause I sure can't!
-Thanks,
A confused CodeIt
P.S.
I've looked at several other sources except the two I named above. I'm just plain confused..
Edit:
The found nil while unwrapping an Optional error I mentioned in the first example is found on this line:
var cellView: NSTableCellView = tableView.makeViewWithIdentifier(tableColumn.identifier, owner: self) as NSTableCellView
// Get an existing cell with the MyView identifier if it exists
var cellView?: NSTableCellView = tableView.makeViewWithIdentifier("someIdentifier", owner: self) as NSTableCellView
// There is no existing cell to reuse so create a new one
if cellView == nil {
cellView = NSTableCellView(frame: NSRect())
// The identifier of the NSTextField instance is set to someIdentifier.
// This allows the cell to be reused.
cellView.identifier = "someIdentifier
}
This should give you the cell, and you can proceed.
See Apple Doc for more

Resources