使用复选标记构建NSOutline视图

时间:2018-07-23 17:39:56

标签: swift macos cocoa nsoutlineview

我正在寻找使用Apple推荐的正确方法向NSOutlineview添加复选框的方法-但是从文档中看不出来。

如何添加允许用户使用的行为,如果我单击父复选框,则它将选择子项,而如果不单击它,它将取消选择该项目的子项?

编辑:我简化了我的问题,并添加了图片以使其更清晰(希望如此)

我的方法: 我一直在使用Code Different提供的出色答案在Mac应用程序中构建“大纲”视图。 https://stackoverflow.com/a/45384599/559760 -我选择使用手动过程而不是使用CocoaBindings填充NSoutLine视图。

我在堆栈视图中添加了一个复选框,这似乎是正确的方法:

Example with the checkbox

我的解决方案包括创建一个数组,以将选定的项保存在viewcontroller中,然后创建用于添加和删除的功能

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

我通过分配给复选框的标签识别该复选框按钮

1 个答案:

答案 0 :(得分:1)

为了将来的Google员工的利益,我将重复我在other answer中编写的内容。此处的区别是,这具有额外的要求,即列是可编辑的,并且我已经改进了该技术。


NSOutlineView的关键是必须为每行提供一个标识符,可以是字符串,数字或唯一标识该行的对象。 NSOutlineView将此称为item。基于此item,您将查询数据模型以填充轮廓。

在此答案中,我们将设置一个具有两列的轮廓视图:一个可编辑的已选中列和一个不可编辑的标题列。


Interface Builder设置

  • 选择第一列并将其标识符设置为isSelected
  • 选择第二列并将其标识符设置为title

set column identifier

  • 在第一列中选​​择单元格,并将其标识符更改为isSelectedCell
  • 在第二列中选择单元格,并将其标识符更改为titleCell

set cell identifier

一致性在这里很重要。单元格的标识符应等于其列的标识符+ Cell


带有复选框的单元格

默认NSTableCellView包含不可编辑的文本字段。我们需要一个复选框,所以我们必须设计自己的单元格。

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

连接插座

  • 删除isSelected列中的默认文本字段
  • 从对象库中拖动复选框
  • 选择NSTableCellView并将其类别更改为CheckboxCellView
  • 打开助手编辑器并连接插座

set custom class and connect the outlet


视图控制器

最后是视图控制器的代码:

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