How to colour individual rows in Cell-Based NSTableVew in Swift 5 - xcode

I am trying to show items in NSTableView but one of them (the item that previously was activated by an action (its name is stored in alreadyActivatedItem variable)) should be disabled and shown with a red text.
So far I managed to make disabling work properly.
I just cannot manage colouring the already activated item be red text. My code below will colour ALL cells' text in red.
extension PreferencesViewController: NSTableViewDelegate {
// disable selecting the already activated item
func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
return !(myArray[row].name == alreadyActivatedItem)
}
// colouring the already activated item in red (it is also disabled)
func tableView(_ tableView: NSTableView, willDisplayCell cell: Any, for tableColumn: NSTableColumn?, row: Int) {
guard let c = cell as? NSTextFieldCell else {
return
}
if c.stringValue == alreadyActivatedItem {
c.textColor = .red
}
}
}
I also tried an other way:
// colouring the already activated item in red (it is also disabled)
func tableView(_ tableView: NSTableView, willDisplayCell cell: Any, for tableColumn: NSTableColumn?, row: Int) {
guard let c = tableColumn?.dataCell(forRow: row) as? NSTextFieldCell else {
return
}
if c.stringValue == alreadyActivatedRow {
c.textColor = .red
}
}
In both cases I will have all the rows with red text:
see as all items are red text
While debugging, I can see that:
let c = cell as? NSTextFieldCell seems to get the current row's cell, at least I get back the row's stringValue correctly with c.stringValue
if c.stringValue == alreadyActivatedRow seems to work good, at least it only steps inside if the condition is true.
So why still do all the items get red colour?
How to achieve my goal then?
(Xcode 11.3.1, Swift 5.1.3)

Cells are reused. You have to add an else clause to set the color always to a defined state.
if c.stringValue == alreadyActivatedItem {
c.textColor = .red
} else {
c.textColor = .black
}
Or simpler
c.textColor = c.stringValue == alreadyActivatedItem ? .red : .black
I recommend even a view based table view and Cocoa Bindings.

Related

View based NSTableView: Empty/white labels on dragging

I setup a trivial view based NSTableView, the view is a simple NSTextField used as a label:
func tableView(_ tv: NSTableView, viewFor tc: NSTableColumn?, row: Int)
-> NSView?
{
let v = (tv.makeView(withIdentifier: viewID, owner: nil) as? NSTextField)
?? NSTextField()
v.isSelectable = false
v.isEditable = false
v.stringValue = data[row] // [String]
v.identifier = viewID
return v
}
and then I enable dragging of the items using this delegate method:
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int)
-> NSPasteboardWriting?
{
return MyPasteboardItem(value: data[row])
}
This works, but when I drag the row, I get an empty representation of the textfield:
(in a different setup things like image views and buttons get drawn, but the NSTextField also ends up white).
I highly suspect this is due to the NSTextField being backed by a TextLayer which doesn't get drawn if the tableview captures an image of the view hierarchy being dragged.
What is a good way to fix this? I considered implementing draw(), but well.
Update: If I do an own NSTextField subclass and override draw(), it indeed starts to work:
final class MyTextField : NSTextField {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
}
Looking at the thing in the view debugger shows that the Layer switches from NSTextLayer to _NSViewBackingLayer when draw is overridden.
But I assume this is not exactly desirable? Is there a better way to accomplish this?
Complete sample: https://gist.github.com/helje5/48728983951ab3362af43b967c554475
Setting drawsBackground=false on the textfield fixed it for me.
If you are using xib file then untick "Draws Background" on the Text Field or in your viewFor: method something like:
v.drawsBackground = false;
I ended up with this in an NSTextField subclass, not sure whether it is a good idea:
override public func draw(_ dirtyRect: NSRect) {
// This switches from the UXLabel being backed by `NSTextLayer` to
// `_NSViewBackingLayer`, which may not be desirable.
// BUT: This enables proper drawing of the Drag&Drop cell.
super.draw(dirtyRect)
}

NSTableView reloadData(forRowIndexes:columnIndexes:) breaks autolayout

I have an NSTableView that can swap in different cell views based on data values for the row. When the model changes, I reload the table, and the table's delegate will provide the right table cell view for the new data.
The table uses autolayout for its cell views. All cell views load normally initially. When updating the table after a model change, I get different results depending on whether I call reloadData() or reloadData(forRowIndexes:columnIndexes). When using reloadData(), the cell view is loaded and autolayout works fine. If I use reloadData(forRowIndexes:columnIndexes), autolayout produces completely different, unexpected results.
I created a sample project to demonstrate the problem.
Here is an image of the project setup including constraints set on the table cell views. There are two row templates, one with a blue view (even rows), one with green (odd rows) that should span the table width (minus a bit of padding). A controller supplies the cell views:
class TableController: NSObject {
#IBOutlet weak var tableView: NSTableView!
var colorData = [1, 0, 1, 0]
#IBAction func swapLine(_ sender: Any) {
colorData[1] = (colorData[1] + 1) % 2
// tableView.reloadData()
tableView.reloadData(forRowIndexes: [1], columnIndexes: [0])
}
}
extension TableController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return colorData.count
}
}
extension TableController: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cellId = (colorData[row]) % 2 == 0 ? "EvenCell" : "OddCell"
return tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(cellId), owner: self)
}
}
A button in the interface just swaps the data for row 1 and reloads the data. The initial view looks like this (alternating green and blue rects). If you use reloadData(), it looks like this (row 1 changed from blue to green). But, if you use reloadData(withRowIndexes:columnIndexes:), the cell view shrinks to 40 points wide vice 480 as in the others. Here's a grab of the view debugger showing the cell view with the wrong size and showing ambiguous width constraints (this doesn't happen when using reloadData()).
The documentation mentions that the row view is reused with reloadData(forRowIndexes:columnIndexes:), but not with reloadData(), which I've verified. I imagine this reusing of the row view is what's causing the autolayout problems, but I can find no connection. Nothing found at SO, AppKit release notes, WWDC videos, Google searches or from pounding my head on the table. Would be truly grateful for assistance.
Update:
Here's the code for ColorView:
class ColorView: NSView {
#IBInspectable var intrinsicHeight: CGFloat = 20
#IBInspectable var color: NSColor = NSColor.blue
override var intrinsicContentSize: NSSize {
return NSSize(width: NSView.noIntrinsicMetric, height: intrinsicHeight)
}
override func draw(_ dirtyRect: NSRect) {
color.setFill()
dirtyRect.fill()
}
}
I think I've got it working. If I call layoutSubtreeIfNeeded() on the cell just before it is returned (so that all its subviews like the dynamic text are already set), then it seems to work.
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
//...
cell.layoutSubtreeIfNeeded()
return cell
}
I hope that helps.
I ran into the same issue, and noticed the actual auto-layout constraints were missing for the rows that reloadData is called for. My (hacky) solution was to add the constraints that are supposed to be automatically set up for the cell manually as well. Note that in my table view I'm just using one column so I'm able to set the width constraint to equal the row's width instead of relying on the columns specified width.
class CustomRowView: NSTableRowView {
override func addSubview(_ view: NSView) {
super.addSubview(view)
// Add constraints NSTableView is supposed to set up
view.topAnchor.constraint(equalTo: topAnchor).isActive = true
view.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
view.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.0).isActive = true
view.layoutSubtreeIfNeeded()
}
}

NSTableView Cell with Identifier keep giving nil

I am working on building MacOS app. I am trying to make table view that updates the cell when I press add button.
Following is my code:
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let identifier = tableColumn?.identifier as NSString?
if ( identifier == "NameCell")
{
var result: NSTableCellView
let cell = tableView.make(withIdentifier: "NameCell", owner: self) as! NSTableCellView
cell.textField?.stringValue = self.data[row].setting!
return cell
}
else if (identifier == "SettingCell")
{
if let cell = tableView.make(withIdentifier: "SettingCell", owner: self) as? NSTableCellView {
cell.textField?.stringValue = self.data[row].setting!
return cell
}
}
return nil
}
However, the line let cell = tableView.make(withIdentifier: "NameCell", owner: self) as! NSTableCellView is keep failing because it returns nil
fatal error: unexpectedly found nil while unwrapping an Optional value
NameCell is from
Can anyone please help me find a way to solve this problem?
For anyone else who comes here with this same question when trying to make an NSTableView fully programmatically: makeView(withIdentifier:owner:) WILL return nil unless a corresponding NIB exists for the given identifier:
NSTableView documentation:
If a view with the specified identifier can’t be instantiated from the nib file or found in the reuse queue, this method returns nil.
Likewise, the 'owner' param is a NIB-specific concept. In short: you cannot use this method if populating your NSTableView with cells programmatically.
In this answer, I detail the Swift code to produce an NSTableCellView programmatically: https://stackoverflow.com/a/51736468/5951226
However, if you don't want all the features of an NSTableViewCell, note that you can return any NSView in tableView(_:viewFor:row:). So you could, as per the CocoaProgrammaticHowtoCollection, simply write:
let cell = NSTextField()
cell.identifier = "my_id" // Essential! Allows re-use of the instance.
// ... Set any properties you want on the NSTextField.
return cell
You should set the "Identifier" with "NameCell" in the NSTableCellView. And your codes should simplified as follow since the column's identifier won't change for ever:
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
var result: NSTableCellView
let cell = tableView.make(withIdentifier: "NameCell", owner: self) as! NSTableCellView
cell.textField?.stringValue = self.data[row].setting!
return cell
}
references settings in XCode Interface Builder:

Programmatically collapse a group row in NSOutlineView

I have an NSOutlineView with an action (see code) that collapse a row when the user clicks anywhere on that row. However it is not working for group.
Some rows are defined as group via the "shouldShowOutlineCellForItem" delegate method.
I can expand a group row programmatically, but not collapse it. Any suggestions?
isExpanded is correctly set via the notifications.
#IBAction func didClick(sender: AnyObject?)
{
assert(self.root != nil)
let selectedRow = outlineView.clickedRow
let proposedItem = (selectedRow == -1) ? self.root! : outlineView.itemAtRow(selectedRow) as! thOutlineNode
if proposedItem.isExpanded
{
self.outlineView.collapseItem(proposedItem)
}
else
{
self.outlineView.expandItem(proposedItem)
}
}
Possibly duplicate. Based on this existing SO question covering Objective-C, try adding the NSOutlineViewDelegate delegate method
func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: AnyObject) -> Bool {
return true
}
to the view controller of your NSOutlineView. From the Apple documentation for the NSOutlineViewDelegate, we see that this is expected behaviour:
optional func outlineView(_ outlineView: NSOutlineView,
shouldShowOutlineCellForItem item: AnyObject) -> Bool
...
Discussion
Returning NO causes frameOfOutlineCellAtRow: to return NSZeroRect,
hiding the cell. In addition, the row will not be collapsible by
keyboard shortcuts.

collect result of NStableView with checkboxes in Swift

I have hard time trying to collect the number of checkboxes checked inside the second column of a NStableView.
I composed a NSTableView with 2 column (via IB),
the first is named : BugColumn (it contains textfiled)
the second is named : CheckedColumn (it contains checkboxes)
Here is the code used to display strings in the first column :
var objets: NSMutableArray! = NSMutableArray()
...
extension MasterViewController: NSTableViewDataSource
{
func numberOfRowsInTableView(aTableView: NSTableView) -> Int
{
return self.objets.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView?
{
var cellView: NSTableCellView = tableView.makeViewWithIdentifier(tableColumn!.identifier, owner: self) as! NSTableCellView
if tableColumn!.identifier == "BugColumn"
{
cellView.textField!.stringValue = self.objets.objectAtIndex(row) as! String
}
return cellView
}
The second column is made of checkboxes appearing for each element of the first column.
I would like to know what is the corresponding text in the first column for each checkboxes enabled (checked).
I red a few exemples about NSTableView but, or they do differents things, or they are in Objective-C.
Could someone explain how to do that using swift?
Thanks

Resources