Setting NSTableView cell text - macos

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

Related

Display Image View in Table View conditionally in Swift

I'm trying to display an image in a table view cell view on the condition of a Boolean value.
The Boolean is a representation of the state of an object of the class "Book" where the objects are initialized:
class Book: NSObject, Codable {
#objc dynamic var author: String
#objc dynamic var title: String
#objc dynamic var lentBy: String
#objc dynamic var available: Bool {
if lentBy == "" {
return true
} else {return false}
}
init(author: String, title: String, lentBy: String) {
self.author = author
self.title = title
self.lentBy = lentBy
}
}
If the String lentBy is not specified, the Bool available returns true: no one has lent the book and hence it should be available. Binding the available object to the table view, the respective table view cell displays either 1 or 0. Instead of 1 or 0 I would like it to display an image: NSStatusAvailable or NSStatusUnavailable.
Have a look at this: https://i.imgur.com/xkp0znT.png.
Where the text field "Geliehen von" (lent by) is empty, the status is 1 and should display the green circle; otherwise a red circle. The green circle you see now is simply dragged into the table cell view and is non-functional. But this is the idea.
Now I'm wondering how to display the respective image view instead of the Bool 1 or 0.
The table view is constructed with the interface builder in a storyboard. If I'm trying to make changes to it programmatically, nothing gets display in the table view anymore. I suppose this is due to the set bindings. Removing the bindings just for the last column doesn't work. This is how I tried it (without implementation of the image view; I don't know how to do that programmatically):
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if tableColumn == tableView.tableColumns[2] {
let cellIdentifier = "statusCellID"
let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: cellIdentifier), owner: self) as? NSTextField
if let cell = cell {
cell.identifier = NSUserInterfaceItemIdentifier(rawValue: cellIdentifier)
cell.stringValue = books[row].lentBy
}
return cell
}
return nil
}
What's the best solution to achieve this? Could I somehow, instead of a Bool, directly return the respective, e.g. CGImage types for lentBys representation available?
You are using Cocoa Bindings. This makes it very easy.
In Interface Builder drag an NSTableCellView with image view into the last column and delete the current one.
Delete the text field and set appropriate constraints for the image view.
Rather than viewForColumn:Row implement
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
return books[row]
}
Extend the model with an image property which is driven by KVO
class Book: NSObject, Codable {
#objc dynamic var author: String
#objc dynamic var title: String
#objc dynamic var lentBy: String
#objc dynamic var available: Bool {
return lentBy.isEmpty
}
#objc dynamic var image: NSImage {
return NSImage(named: (lentBy.isEmpty) ? NSImage.statusAvailableName : NSImage.statusUnavailableName)!
}
static func keyPathsForValuesAffectingImage() -> Set<String> { return ["lentBy"] }
init(author: String, title: String, lentBy: String) {
self.author = author
self.title = title
self.lentBy = lentBy
}
}
In Interface Builder bind the Value of the image view of the table cell view to Table Cell View > objectValue.image

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.

Swift 3 Load xib. NSBundle.mainBundle().loadNibNamed return Bool

I was trying to figure out how to create a custom view using xib files.
In this question the next method is used.
NSBundle.mainBundle().loadNibNamed("CardView", owner: nil, options: nil)[0] as! UIView
Cocoa has the same method,however, this method has changed in swift 3 to loadNibNamed(_:owner:topLevelObjects:), which returns Bool, and previous code generates "Type Bool has no subscript members" error, which is obvious, since the return type is Bool.
So, my question is how to a load view from xib file in Swift 3
First of all the method has not been changed in Swift 3.
loadNibNamed(_:owner:topLevelObjects:) has been introduced in macOS 10.8 and was present in all versions of Swift. However loadNibNamed(nibName:owner:options:) has been dropped in Swift 3.
The signature of the method is
func loadNibNamed(_ nibName: String,
owner: Any?,
topLevelObjects: AutoreleasingUnsafeMutablePointer<NSArray>?) -> Bool
so you have to create an pointer to get the array of the views on return.
var topLevelObjects = NSArray()
if Bundle.main.loadNibNamed("CardView", owner: self, topLevelObjects: &topLevelObjects) {
let views = (topLevelObjects as Array).filter { $0 is NSView }
return views[0] as! NSView
}
Edit: I updated the answer to filter the NSView instance reliably.
In Swift 4 the syntax slightly changed and using first(where is more efficient:
var topLevelObjects : NSArray?
if Bundle.main.loadNibNamed(assistantNib, owner: self, topLevelObjects: &topLevelObjects) {
return topLevelObjects!.first(where: { $0 is NSView }) as? NSView
}
Swift 4 version of #vadian's answer
var topLevelObjects: NSArray?
if Bundle.main.loadNibNamed(NSNib.Name(rawValue: nibName), owner: self, topLevelObjects: &topLevelObjects) {
return topLevelObjects?.first(where: { $0 is NSView } ) as? NSView
}
I wrote an extension that is safe and makes it easy to load from nib:
extension NSView {
class func fromNib<T: NSView>() -> T? {
var viewArray = NSArray()
guard Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, topLevelObjects: &viewArray) else {
return nil
}
return viewArray.first(where: { $0 is T }) as? T
}
}
Then just use like this:
let view: CustomView = .fromNib()
Whether CustomView is a NSView subclass and also CustomView.xib.

NSTableView old selection

How do I get the previous selected index of row in NSTableView
The methods
func tableViewSelectionIsChanging(notification: NSNotification) {
let index = (notification.object as! NSTableView).selectedRow
println(index)
}
func tableViewSelectionDidChange(notification: NSNotification)
let index = (notification.object as! NSTableView).selectedRow
println(index)
}
Both methods print same value. How do I get the index / row which is going to change due to selection ?
Or in simple terms, how do I make a statement like
println("\(Selection changed from \(tableView.oldSelectedIndex) to \(tableView.newSelectionIndex)")
Before a cell is selected, the function func tableView(tableView: NSTableView, shouldSelectRow row: Int) is called on the delegate. Here, you can see both the currently selected row, and the proposed selection.
func tableView(tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
// This is the next index
NSLog("Next index: \(row)")
// Make sure that a row is currently selected!
guard self.tableView.selectedRowIndexes.count > 0 else {
return true
}
// Get the currently selected row.
let index = self.tableView.selectedRow
NSLog("Previous index: \(index)")
return true
}
NB: This doesn't exactly answer your question, because you've asked for the change after the new selection has occurred, whereas this gives it just before. I hope nonetheless that this is sufficient.
You can get it by
tableView.selectedRow

How to get custom table cell views into NSTableView?

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).

Resources