collect result of NStableView with checkboxes in Swift - xcode

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

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.

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;
}

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:

Updated NSTableCellView.textField NOT updated in table display.

I have a feeling this table cell display problem has a simple answer but, I clearly need greater wisdom than mine to find it.
Here's my textbook controller, delegate and datasource class for the table and the enclosing view ...
import Cocoa
class Table8020ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
var tokenText: [String] = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta"]
override func viewDidLoad() {
super.viewDidLoad()
}
func numberOfRowsInTableView(aTableView: NSTableView) -> Int {
println("numberOfRowsInTableView = \(tokenText.count)")
return tokenText.count
}
func tableView(aTableView: NSTableView, objectValueForTableColumn aTableColumn: NSTableColumn?, row rowIndex: Int) -> AnyObject? {
var result = aTableView.makeViewWithIdentifier(aTableColumn!.identifier, owner: self) as! NSTableCellView
println("textField in = \(result.textField!.stringValue)")
result.textField!.stringValue = tokenText[rowIndex]
println("textField out = \(result.textField!.stringValue)")
return result
}
}
I log the updates to the .textField which seems to work ok.
numberOfRowsInTableView = 6
textField in = Table View Cell
textField out = alpha
textField in = Table View Cell
textField out = beta
textField in = Table View Cell
textField out = gamma
textField in = Table View Cell
textField out = delta
textField in = Table View Cell
textField out = epsilon
textField in = Table View Cell
textField out = zeta
But the actual table display retains the original interface builder values! 'Table View Cell' Something mysterious appears to be happening after after the 'return result'.
I'm using the latest Xcode Version 6.3 (6D570) with Swift V1.2
You're making a couple of mistakes.
Firstly you're returning the wrong kind of value from tableView:objectValueForTableColumn:row. This method isn't requesting a view instance, it's requesting the object that your view instance will be representing. In other words it wants to know what model object the view will be displaying. In each case your model object is very simple - it's one of the strings in the tokenText array.
func tableView(tableView: NSTableView,
objectValueForTableColumn tableColumn: NSTableColumn?,
row: Int) -> AnyObject? {
return tokenText[row]
}
Secondly you've failed to implement tableView:viewForTableColumn:row:. This is where you create a view for each cell in your table and tell that view which bits of the model object you want to display. In each case your model object is just a string, so you essentially tell the view to display the entire model object in its textField:
func tableView(tableView: NSTableView,
viewForTableColumn tableColumn: NSTableColumn?,
row: Int) -> NSView? {
var view = tableView.makeViewWithIdentifier(tableColumn!.identifier,
owner: self) as! NSTableCellView
view.textField!.stringValue = tokenText[row]
return view
}

Resources