NSTableCellView shows nothing if it's a group cell - cocoa

I'm trying to make a simple view-based NSTableView with groups and regular cells. Every cell is drawn correct and shows everything I need unless I add this method to my NSTableViewDelegate/DataSource:
func tableView(tableView: NSTableView, isGroupRow row: Int) -> Bool {
return (tableContent[row] as NSDictionary)["group"] != nil
}
It works just fine, but cells that become group cells show nothing. I've tried it on different cell types, but still nothing, just a semi-transparent gray background. Even if I add something like
override func drawRect(dirtyRect: NSRect) {
NSColor(calibratedRed: 0, green: 255, blue: 0, alpha: 1).setFill()
NSRectFill(dirtyRect)
super.drawRect(dirtyRect)
// Drawing code here.
}
to my group cell class I'll get only a semi-transparent gray background.

I think maybe you make the same mistake as I did today.
When using func tableView(tableView: NSTableView, isGroupRow row: Int) -> Bool, the func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? no longer provides tableColumn for grouped rows, which means tableColumn is nil. So if you implementation relies on tableColumn.identifier, it is unreliable.
You should change your implementation to relying on row with grouped rows.

Related

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

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.

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()
}
}

Merge cells of NSTableView

Is there any way to merge cell's in a NSTableView?
I know there are two modes for displaying data in a NSTableView: cell-based and view-based. If the data are presented by cells, the function
tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int)
is responsable, otherwise
tableView(_ tableView: NSTableView, rowViewForRow row: Int)
Currently I'm using the first one. In iOS, you can use the cell prototype in interface builder to link the views with a custom UITableViewCell, but how can I achieve this for a complete row in a NSTableView?
Maybe I found a solution: You can use both
tableView(_:viewFor:row:)
and
tableView(:viewFor:tableColumn:row:)
but you have to be aware that the tableView is not using both methods for one cell (mutually exclusive).
Now you can define a xib, with a single UIView in it. Change the class of the UIView to NSTableRowView. Next, load the class in viewDidLoad into your table:
if let nib: NSNib = NSNib(nibNamed: NSNib.Name("SpecialRow"), bundle: nil) {
self.tableView?.register(nib, forIdentifier: NSUserInterfaceItemIdentifier("specialRow"));
}
In your table method rowViewForRow you can do the following:
if let srow: SpecialRow = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "specialRow"), owner: nil) as? SpecialRow {
// ...
return srow;
}

Setting Background Color of NSTableCellView in Swift

After searching through SO and online, I'm struggling to figure out a concept that I thought would be relatively simple. Essentially, I have a table in an OS X Swift app, with several columns, and it is currently populating data. I am trying to discern how I can set the background color of each "row" (ideally with alternating colors, but I'll start with just one color). My MasterViewController file is like so;
import Cocoa
class MasterViewController: NSViewController {
var minions = [Minion]()
func setupSampleMinion() {
minions = Minion.fetchMinionData()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
}
// MARK: - NSTableViewDataSource extension MasterViewController: NSTableViewDataSource {
func numberOfRowsInTableView(aTableView: NSTableView) -> Int {
return self.minions.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
// 1
var cellView: NSTableCellView = tableView.makeViewWithIdentifier(tableColumn!.identifier, owner: self) as! NSTableCellView
let minion = self.minions[row]
// 2
if tableColumn!.identifier == "MyColumn" {
// 3
cellView.imageView!.image = NSImage(named: "minion.name!")
cellView.textField!.stringValue = minion.name!
return cellView
}
return cellView
}
}
func tableView(tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
let myCustomView = MyRowView()
return myCustomView
}
class MyRowView: NSTableRowView {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
self.backgroundColor = NSColor(red: 0.76, green: 0.82, blue: 0.92, alpha: 1)
NSRectFill(dirtyRect)
}
}
// MARK: - NSTableViewDelegate extension MasterViewController: NSTableViewDelegate {
}
While I THINK I have some of the coding right, this does not seem to set the background color if the row in any way. Any thoughts or overall guidance would be most appreciated. Thank you!
If you just want the rows to use the standard alternating colors for rows, there's a simple checkbox in the Attributes inspector for the table view in IB to enable that.
To use a non-standard background color, you want to set the row view's backgroundColor, but not inside of drawRect(). If you change properties of a view that affect how it draws inside of drawRect(), that will probably mark the view as needing display, which will provoke another call to drawRect(), etc.
It should work to just set it in the delegate's tableView(_:didAddRowView:forRow:) method. That's documented in the description of the backgroundColor property.
With regard to your attempt at overriding drawRect(): setting the row view's backgroundColor will presumably affect how the superclass draws. So, setting it after calling through to super is unlikely to help. It definitely won't affect the subsequent NSRectFill() call. That function relies on the fill color set for the current graphics context, which is implicit. You would change that by calling someColor.set().
Buy, anyway, there should be no need to override drawRect() given that you can set the backgroundColor. If you want to achieve some background drawing beyond what's possible by just setting a color, you should override drawBackgroundInRect() and not drawRect(), anyway.
Finally, your implementation of tableView(tableView:rowViewForRow:) should call the table view's makeViewWithIdentifier(_:owner:) method first, before creating a new view. And it should set the identifier on any new view it does create. That allows the table view to maintain a reuse queue of views, to avoid constantly destroying and recreating views.

Resources