如何在没有绑定的情况下使用 NSOutlineView

How to use NSOutlineView without bindings

我的数据如下:

let keysDetails : [String : Any] = [
    "kAddToBag" : [
        "locDict" : [
            "at" : "add",
            "ae" : "Add"
        ],
        "jsonDict" : [
            "at" : "add to bag",
            "ae" : "ADD TO BAG"
        ],
        "path" : "somepath"
    ],
    "kShopTab" : [
        "locDict" : [
            "be_fr" : "shop",
            "be_nl" : "SHOP"
        ],
        "jsonDict" : [
            "be_fr" : "shop",
            "be_nl" : "SHOP"
        ],
        "path" : "somepath2"
    ]
]

我希望 NSOutlineView 看起来像上图。

输入数据

let keysDetails : [String : Any] = [
    "kAddToBag" : [
        "locDict" : [
            "at" : "add",
            "ae" : "Add"
        ],
        "jsonDict" : [
            "at" : "add to bag",
            "ae" : "ADD TO BAG"
        ],
        "path" : "somepath"
    ],
    "kShopTab" : [
        "locDict" : [
            "be_fr" : "shop",
            "be_nl" : "SHOP"
        ],
        "jsonDict" : [
            "be_fr" : "shop",
            "be_nl" : "SHOP"
        ],
        "path" : "somepath2"
    ]
]

字典是无序的 collection,当您想要通过索引访问元素时,它会导致问题,...让我们创建一个自定义结构,为 NSOutlineView 提供数据。

struct Item {
    let title: String      // First column value
    let loc: String        // Second column value
    let json: String       // Third column value
    let children: [Item]   // Possible children

    init(title: String, loc: String, json: String, children: [Item] = []) {
        self.title = title
        self.loc = loc
        self.json = json
        self.children = children
    }

    init?(title: String, content: Any) {
        // Check that the content is a dictionary and that it contains
        // locDict & jsonDict and both are dictionaries
        guard let content = content as? [String: Any],
            let loc = content["locDict"] as? [String: String],
            let json = content["jsonDict"] as? [String: String] else {
                return nil
        }

        // Check that both dictionaries contains same keys
        let locKeys = loc.keys.sorted()
        let jsonKeys = json.keys.sorted()
        guard locKeys == jsonKeys else {
            return nil
        }

        // Initialize top level item
        self.title = title
        self.loc = "locDict"
        self.json = "jsonDict"
        self.children = locKeys.map { key in
            // We can force unwrap here because we already checked that
            // both dictionaries contains same keys
            Item(title: key, loc: loc[key]!, json: json[key]!)
        }
    }
}

这个结构是一个例子,但是还有很多其他的 方法。这真的取决于你打算在这里做什么。你可以切换 到 object(而不是结构),...

这里的关键点是 children 属性 是一个有序的 collection (数组)。

视图控制器

添加 items 属性(同样,一个有序的 collection = 数组)。

class ViewController: NSViewController {
    private let items: [Item] = {
        // Map keysDetails to an array of our Item structures
        keysDetails.compactMap { (key: String, value: Any) in
            Item(title: key, content: value)
        }
    }()
}

NSOutlineViewDataSource

顾名思义,数据源只是提供数据。我们已经有了 items属性,用起来吧

extension ViewController: NSOutlineViewDataSource {
    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        if item == nil {
            // item == nil
            // We're being asked for the number of top level elements (kAddToBag, ...)
            return items.count
        }

        // Develop time (debug) - check that the item is really Item
        assert(item is Item);

        // item != nil
        // We're being asked for the number of children of an item
        return (item as! Item).children.count
    }

    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        if item == nil {
            // item == nil
            // We're being asked for n-th (index) top level element
            return items[index]
        }

        // Develop time (debug) - check that the item is really Item
        assert(item is Item);

        // item != nil
        // We're being asked for n-th (index) child of an item
        return (item as! Item).children[index]
    }

    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        // Develop time (debug) - check that the item is really Item
        assert(item is Item);

        // Item is expandable only if it has children
        return (item as! Item).children.count > 0
    }
}

NSOutlineViewDelegate

除其他外,委托还提供单元格视图以显示特定项目 和专栏。

extension ViewController: NSOutlineViewDelegate {
    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        // Get the column identifier and our Item
        guard let columnIdentifier = tableColumn?.identifier,
            let item = item as? Item else {
                return nil
        }

        // Get a cell view identifier and an actual value we should display
        let cellViewIdentifier: String
        let stringValue: String

        switch columnIdentifier.rawValue {
        case "TitleColumn":
            cellViewIdentifier = "TitleCell"
            stringValue = item.title
        case "LocColumn":
            cellViewIdentifier = "LocCell"
            stringValue = item.loc
        case "JsonColumn":
            cellViewIdentifier = "JsonCell"
            stringValue = item.json
        default:
            return nil
        }

        // Make a view from the cell view identifier
        let view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(cellViewIdentifier), owner: self) as? NSTableCellView
        // Update text field value
        view?.textField?.stringValue = stringValue
        return view
    }
}

示例项目

  • 在 Xcode(macOS - Swift 和 Storyboard)中创建一个新应用
  • 添加大纲视图
    • 设置约束
    • 将委托和数据源连接到视图控制器
  • 点击大纲视图并设置
    • 列数:3
    • 取消选中 Headers,重新排序
  • 将第 1、2 和 3 列标识符设置为 TitleColumnLocColumnJsonColumn
  • 将第 1、2 和 3 列 table 单元格视图标识符设置为 TitleCellLocCellJsonCell