问题
I am looking to add checkboxes to NSOutlineview using the correct Apple recommended method - however its not clear from the documentation.
How do I add the behavour to allow users whereby if I click a parent checkbox, then it will select the children, and if I unclick it - it will deselect the children of that item?
edit: I have simplified my question and added image to make it clearer ( hopefully)
My Approach: I have been using the wonderful answer by Code Different to build an Outline view in my mac app. https://stackoverflow.com/a/45384599/559760 - I chose to populate the NSoutLine view using a manual process instead of using CocoaBindings.
I added in a stack view including a check box which seems to be the right approach:
My solution involves creating an array to hold the selected items in the viewcontroller and then creating functions for adding and removing
var selectedItems: [Int]?
@objc func cellWasClicked(sender: NSButton) {
let newCheckBoxState = sender.state
let tag = sender.tag
switch newCheckBoxState {
case NSControl.StateValue.on:
print("adding- \(sender.tag)")
case NSControl.StateValue.off:
print("removing- \(sender.tag)")
default:
print("unhandled button state \(newCheckBoxState)")
}
I identify the checkbutton by the tag that was assigned to the checkbox
回答1:
In the interest of future Googlers I will repeat things I've written in my other answer. The difference here is this has the extra requirement that a column is editable and I have refined the technique.
The key to NSOutlineView is that you must give an identifier to each row, be it a string, a number or an object that uniquely identifies the row. NSOutlineView calls this the item. Based on this item, you will query your data model to populate the outline.
In this answer we will setup an outline view with 2 columns: an editable Is Selected column and a non-editable Title column.
Interface Builder setup
- Select the first column and set its identifier to
isSelected - Select the second column and set its identifier to
title
- Select the cell in the first column and change its identifier to
isSelectedCell - Select the cell in the second column and change its identifier to
titleCell
Consistency is important here. The cell's identifier should be equal to its column's identifier + Cell.
The cell with a checkbox
The default NSTableCellView contains a non-editable text field. We want a check box so we have to design our own cell.
CheckboxCellView.swift
import Cocoa
/// A set of methods that `CheckboxCelView` use to communicate changes to another object
protocol CheckboxCellViewDelegate {
func checkboxCellView(_ cell: CheckboxCellView, didChangeState state: NSControl.StateValue)
}
class CheckboxCellView: NSTableCellView {
/// The checkbox button
@IBOutlet weak var checkboxButton: NSButton!
/// The item that represent the row in the outline view
/// We may potentially use this cell for multiple outline views so let's make it generic
var item: Any?
/// The delegate of the cell
var delegate: CheckboxCellViewDelegate?
override func awakeFromNib() {
checkboxButton.target = self
checkboxButton.action = #selector(self.didChangeState(_:))
}
/// Notify the delegate that the checkbox's state has changed
@objc private func didChangeState(_ sender: NSObject) {
delegate?.checkboxCellView(self, didChangeState: checkboxButton.state)
}
}
Connecting the outlet
- Delete the default text field in the
isSelectedcolumn - Drag in a checkbox from Object Library
- Select the
NSTableCellViewand change its class toCheckboxCellView - Turn on the Assistant Editor and connect the outlet
The View Controller
And finally the code for the view controller:
import Cocoa
/// A class that represents a row in the outline view. Add as many properties as needed
/// for the columns in your outline view.
class OutlineViewRow {
var title: String
var isSelected: Bool
var children: [OutlineViewRow]
init(title: String, isSelected: Bool, children: [OutlineViewRow] = []) {
self.title = title
self.isSelected = isSelected
self.children = children
}
func setIsSelected(_ isSelected: Bool, recursive: Bool) {
self.isSelected = isSelected
if recursive {
self.children.forEach { $0.setIsSelected(isSelected, recursive: true) }
}
}
}
/// A enum that represents the list of columns in the outline view. Enum is preferred over
/// string literals as they are checked at compile-time. Repeating the same strings over
/// and over again are error-prone. However, you need to make the Column Identifier in
/// Interface Builder with the raw value used here.
enum OutlineViewColumn: String {
case isSelected = "isSelected"
case title = "title"
init?(_ tableColumn: NSTableColumn) {
self.init(rawValue: tableColumn.identifier.rawValue)
}
var cellIdentifier: NSUserInterfaceItemIdentifier {
return NSUserInterfaceItemIdentifier(self.rawValue + "Cell")
}
}
class ViewController: NSViewController {
@IBOutlet weak var outlineView: NSOutlineView!
/// The rows of the outline view
let rows: [OutlineViewRow] = {
var child1 = OutlineViewRow(title: "p1-child1", isSelected: true)
var child2 = OutlineViewRow(title: "p1-child2", isSelected: true)
var child3 = OutlineViewRow(title: "p1-child3", isSelected: true)
let parent1 = OutlineViewRow(title: "parent1", isSelected: true, children: [child1, child2, child3])
child1 = OutlineViewRow(title: "p2-child1", isSelected: true)
child2 = OutlineViewRow(title: "p2-child2", isSelected: true)
child3 = OutlineViewRow(title: "p2-child3", isSelected: true)
let parent2 = OutlineViewRow(title: "parent2", isSelected: true, children: [child1, child2, child3])
child1 = OutlineViewRow(title: "p3-child1", isSelected: true)
child2 = OutlineViewRow(title: "p3-child2", isSelected: true)
child3 = OutlineViewRow(title: "p3-child3", isSelected: true)
let parent3 = OutlineViewRow(title: "parent3", isSelected: true, children: [child1, child2, child3])
child3 = OutlineViewRow(title: "p4-child3", isSelected: true)
child2 = OutlineViewRow(title: "p4-child2", isSelected: true, children: [child3])
child1 = OutlineViewRow(title: "p4-child1", isSelected: true, children: [child2])
let parent4 = OutlineViewRow(title: "parent4", isSelected: true, children: [child1])
return [parent1, parent2, parent3, parent4]
}()
override func viewDidLoad() {
super.viewDidLoad()
outlineView.dataSource = self
outlineView.delegate = self
}
}
extension ViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
/// Returns how many children a row has. `item == nil` means the root row (not visible)
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
switch item {
case nil: return rows.count
case let row as OutlineViewRow: return row.children.count
default: return 0
}
}
/// Returns the object that represents the row. `NSOutlineView` calls this the `item`
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
switch item {
case nil: return rows[index]
case let row as OutlineViewRow: return row.children[index]
default: return NSNull()
}
}
/// Returns whether the row can be expanded
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
switch item {
case nil: return !rows.isEmpty
case let row as OutlineViewRow: return !row.children.isEmpty
default: return false
}
}
/// Returns the view that display the content for each cell of the outline view
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
guard let item = item as? OutlineViewRow, let column = OutlineViewColumn(tableColumn!) else { return nil }
switch column {
case .isSelected:
let cell = outlineView.makeView(withIdentifier: column.cellIdentifier, owner: self) as! CheckboxCellView
cell.checkboxButton.state = item.isSelected ? .on : .off
cell.delegate = self
cell.item = item
return cell
case .title:
let cell = outlineView.makeView(withIdentifier: column.cellIdentifier, owner: self) as! NSTableCellView
cell.textField?.stringValue = item.title
return cell
}
}
}
extension ViewController: CheckboxCellViewDelegate {
/// A delegate function where we can act on update from the checkbox in the "Is Selected" column
func checkboxCellView(_ cell: CheckboxCellView, didChangeState state: NSControl.StateValue) {
guard let item = cell.item as? OutlineViewRow else { return }
// The row and its children are selected if state == .on
item.setIsSelected(state == .on, recursive: true)
// This is more efficient than calling reload on every child since collapsed children are
// not reloaded. They will be reloaded when they become visible
outlineView.reloadItem(item, reloadChildren: true)
}
}
Result
来源:https://stackoverflow.com/questions/51484452/building-an-nsoutline-view-with-check-marks