如何编写 NSOutlineView 程序?

12

我在使用Xcode 8 (Swift 3)创建NSOutlineView时遇到了麻烦。 我有一个plist文件,其中包含一些信息,我想在OutlineView中呈现它们。 plist文件如下所示(示例):

Root                      Dictionary    *(1 item)
    Harry Watson          Dictionary    *(5 items)*
        name              String        Harry Watson
        age               Int           99
        birthplace        String        Westminster
        birthdate         Date          01/01/1000
        hobbies           Array         *(2 items)*
            item 0        String        Tennis
            item 1        String        Piano

OutlineView 应该看起来相当类似,如下所示:

name            Harry Watson
age             99
birthplace      Westminster
birthdate       01/01/1000
> hobbies       ...             (<- this should be expandable)

我已经在Google上搜索了关于NSOutlineView的教程,但是我发现所有的都来自raywenderlich.com。我阅读了一些内容,但我认为这并不容易理解。

所以我想知道你是否能帮我提供一些代码示例,针对上述功能尤其是。

func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {}

我不确定在那里写什么。

如果您有任何问题,请让我知道。

提前致谢,祝好。


@ElTomato 嘿,谢谢你的评论 - 你是什么意思?那是我的自己的例子!?如果你认为这很容易,你能帮我吗?我会非常感激你的帮助。 - j3141592653589793238
你尝试了什么? - Willeke
1
@Willeke,我在raywenderlich上重新构建了示例,然后尝试了一下,但我仍然很难理解整个过程。 - j3141592653589793238
@Willeke 你好,我现在稍微修改了我的问题,我尝试了一些东西,但仍然在我问题中提到的委托函数方面遇到困难(在底部)。你能帮我解决这个问题吗?先感谢你! - j3141592653589793238
1
确实如此。但我不会将NSOutlineView与罗马相比较。无论如何,谢谢! - j3141592653589793238
2个回答

33

我发现 Ray Wenderlitch 的教程质量参差不齐。内部笑话、冗长的叙述以及假定你对 Swift 一无所知的逐步手把手指导让我感到非常恶心。这里有一个简单的教程,它涵盖了手动和通过 Cocoa Bindings 填充轮廓视图的基础知识。


理解 NSOutlineView 的关键在于你必须为每一行分配一个唯一的标识符,可以是字符串、数字或表示该行的对象。 NSOutlineView 称之为 item。根据此 item,您将查询数据模型以填充轮廓视图中的数据。

本答案介绍了 3 种方法:

  1. 手动:以最基本的方式自己完成所有操作。 这是学习如何与 NSOutlineView 交互的绝佳介绍,但我不建议用于生产代码。
  2. 简化:轮廓视图仍然是手动填充的,但方法更加优雅。 这是我自己生产代码使用的方法。
  3. Cocoa Binding:Mac OS X 黄金时代留下的一些神奇东西。虽然非常方便,但不是未来的趋势。 将其视为高级主题。

1. 手动填充轮廓视图

Interface Builder 设置

我们将使用一个非常简单的 NSOutlineView,仅具有两个列:Key 和 Value。

选择第一列并将其标识符更改为 keyColumn。 然后选择第二列并将其标识符更改为 valueColumn

Set the identifier for the Key column. Repeat for the Value column

将单元格的标识符设置为 outlineViewCell。您只需要这样做一次。 Set the identifier for the cell

代码

将以下内容复制并粘贴到您的 ViewController.swift 中:

// Data model
struct Person {
    var name: String
    var age: Int
    var birthPlace: String
    var birthDate: Date
    var hobbies: [String]
}

class ViewController: NSViewController {
    @IBOutlet weak var outlineView: NSOutlineView!

    // I assume you know how load it from a plist so I will skip
    // that code and use a constant for simplicity
    let person = Person(name: "Harry Watson", age: 99, birthPlace: "Westminster",
                        birthDate: DateComponents(calendar: .current, year: 1985, month: 1, day: 1).date!,
                        hobbies: ["Tennis", "Piano"])

    let keys = ["name", "age", "birthPlace", "birthDate", "hobbies"]

    override func viewDidLoad() {
        super.viewDidLoad()
        outlineView.dataSource = self
        outlineView.delegate = self
    }
}

extension ViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {

    // You must give each row a unique identifier, referred to as `item` by the outline view
    //   * For top-level rows, we use the values in the `keys` array
    //   * For the hobbies sub-rows, we label them as ("hobbies", 0), ("hobbies", 1), ...
    //     The integer is the index in the hobbies array
    //
    // item == nil means it's the "root" row of the outline view, which is not visible
    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        if item == nil {
            return keys[index]
        } else if let item = item as? String, item == "hobbies" {
            return ("hobbies", index)
        } else {
            return 0
        }
    }

    // Tell how many children each row has:
    //    * The root row has 5 children: name, age, birthPlace, birthDate, hobbies
    //    * The hobbies row has how ever many hobbies there are
    //    * The other rows have no children
    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        if item == nil {
            return keys.count
        } else if let item = item as? String, item == "hobbies" {
            return person.hobbies.count
        } else {
            return 0
        }
    }

    // Tell whether the row is expandable. The only expandable row is the Hobbies row
    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        if let item = item as? String, item == "hobbies" {
            return true
        } else {
            return false
        }
    }

    // Set the text for each row
    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        guard let columnIdentifier = tableColumn?.identifier.rawValue else {
            return nil
        }
    
        var text = ""
    
        // Recall that `item` is the row identiffier
        switch (columnIdentifier, item) {
        case ("keyColumn", let item as String):
            switch item {
            case "name":
                text = "Name"
            case "age":
                text = "Age"
            case "birthPlace":
                text = "Birth Place"
            case "birthDate":
                text = "Birth Date"
            case "hobbies":
                text = "Hobbies"
            default:
                break
            }
        case ("keyColumn", _):
            // Remember that we identified the hobby sub-rows differently
            if let (key, index) = item as? (String, Int), key == "hobbies" {
                text = person.hobbies[index]
            }
        case ("valueColumn", let item as String):
            switch item {
            case "name":
                text = person.name
            case "age":
                text = "\(person.age)"
            case "birthPlace":
                text = person.birthPlace
            case "birthDate":
                text = "\(person.birthDate)"
            default:
                break
            }
        default:
            text = ""
        }
    
        let cellIdentifier = NSUserInterfaceItemIdentifier("outlineViewCell")
        let cell = outlineView.makeView(withIdentifier: cellIdentifier, owner: self) as! NSTableCellView
        cell.textField!.stringValue = text
    
        return cell
    }
}

结果

NSOutlineView


2. 更简洁的方法

按照 #1 的方式设置你的 Storyboard。然后将以下代码复制并粘贴到你的视图控制器中:

import Cocoa

/// The data Model
struct Person {
    var name: String
    var age: Int
    var birthPlace: String
    var birthDate: Date
    var hobbies: [String]
}

/// Representation of a row in the outline view
struct OutlineViewRow {
    var key: String
    var value: Any?
    var children = [OutlineViewRow]()
    
    static func rowsFrom( person: Person) -> [OutlineViewRow] {
        let hobbiesChildren = person.hobbies.map { OutlineViewRow(key: $0) }
        return [
            OutlineViewRow(key: "Age", value: person.age),
            OutlineViewRow(key: "Birth Place", value: person.birthPlace),
            OutlineViewRow(key: "Birth Date", value: person.birthDate),
            OutlineViewRow(key: "Hobbies", children: hobbiesChildren)
        ]
    }
}

/// A listing of all available columns in the outline view.
///
/// Since repeating string literals is error prone, we define them in a single location here.
/// The literals must match the column identifiers in the Story Board
enum OutlineViewColumn: String {
    case key = "keyColumn"
    case value = "valueColumn"
    
    init?(tableColumn: NSTableColumn) {
        self.init(rawValue: tableColumn.identifier.rawValue)
    }
}


class ViewController: NSViewController {
    @IBOutlet weak var outlineView: NSOutlineView!
    
    let person = Person(name: "Harry Watson", age: 99, birthPlace: "Westminster",
                        birthDate: DateComponents(calendar: .current, year: 1985, month: 1, day: 1).date!,
                        hobbies: ["Tennis", "Piano"])
    var rows = [OutlineViewRow]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.rows = OutlineViewRow.rowsFrom(person: self.person)
        outlineView.dataSource = self
        outlineView.delegate = self
    }
}

extension ViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
    
    /// Return the item representing each row
    /// If item == nil, it is the root of the outline view and is invisible
    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        switch item {
        case nil:
            return self.rows[index]
        case let row as OutlineViewRow:
            return row.children[index]
        default:
            return NSNull()
        }
    }
    
    /// Return the number of children for each row
    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        switch item {
        case nil:
            return self.rows.count
        case let row as OutlineViewRow:
            return row.children.count
        default:
            return 0
        }
    }
    
    /// Determine if the row is expandable
    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        switch item {
        case let row as OutlineViewRow:
            return !row.children.isEmpty
        default:
            return false
        }
    }
    
    /// Return content of the cell
    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        guard let row = item as? OutlineViewRow,
              let column = OutlineViewColumn(tableColumn: tableColumn!)
        else {
            fatalError("Invalid row and column combination")
        }
        
        let text: String
        switch column {
        case .key:
            text = row.key
        case .value:
            text = row.value == nil ? "" : "\(row.value!)"
        }
        
        let identifier = NSUserInterfaceItemIdentifier("outlineViewCell")
        let view = outlineView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView
        view.textField?.stringValue = text
        
        return view
    }
}

3. 使用Cocoa Bindings

另一种填充大纲视图的方法是使用Cocoa Bindings,它可以显著减少你需要编写的代码量。但是请注意,Cocoa Bindings是一个高级话题。当它工作时,就像魔术一样,但当它不工作时,修复起来可能非常困难。Cocoa Bindings在iOS上不可用。

代码

对于这个示例,让我们提高要求,让NSOutlineView显示多个人的详细信息。

// Data Model
struct Person {
    var name: String
    var age: Int
    var birthPlace: String
    var birthDate: Date
    var hobbies: [String]
}

// A wrapper object that represents a row in the Outline View
// Since Cocoa Binding relies on the Objective-C runtime, we need to mark this
// class with @objcMembers for dynamic dispatch
@objcMembers class OutlineViewRow: NSObject {
    var key: String                 // content of the Key column
    var value: Any?                 // content of the Value column
    var children: [OutlineViewRow]  // set to an empty array if the row has no children

    init(key: String, value: Any?, children: [OutlineViewRow]) {
        self.key = key
        self.value = value
        self.children = children
    }

    convenience init(person: Person) {
        let hobbies = person.hobbies.map { OutlineViewRow(key: $0, value: nil, children: []) }
        let children = [
            OutlineViewRow(key: "Age", value: person.age, children: []),
            OutlineViewRow(key: "Birth Place", value: person.birthPlace, children: []),
            OutlineViewRow(key: "Birth Date", value: person.birthDate, children: []),
            OutlineViewRow(key: "Hobbies", value: nil, children: hobbies)
        ]
        self.init(key: person.name, value: nil, children: children)
    }
}

class ViewController: NSViewController {
    let people = [
        Person(name: "Harry Watson", age: 99, birthPlace: "Westminster",
                birthDate: DateComponents(calendar: .current, year: 1985, month: 1, day: 1).date!,
                hobbies: ["Tennis", "Piano"]),
        Person(name: "Shelock Holmes", age: 164, birthPlace: "London",
               birthDate: DateComponents(calendar: .current, year: 1854, month: 1, day: 1).date!,
                hobbies: ["Violin", "Chemistry"])
    ]

    @objc lazy var rows = people.map { OutlineViewRow(person: $0) }

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Interface Builder设置

在你的Storyboard中:

  • 从Object Library中添加一个Tree Controller
  • 选择Tree Controller并打开Attributes Inspector(Cmd+Opt+4)。将其Children key path设置为children
  • 打开Bindings inspector(Cmd+Opt+7)并设置IB对象的绑定如下。

Tree Controller's Attributes

| IB Object       | Property           | Bind To         | Controller Key  | Model Key Path    |
|-----------------|--------------------|-----------------|-----------------|-------------------|
| Tree Controller | Controller Content | View Controller |                 | self.rows         |
| Outline View    | Content            | Tree Controller | arrangedObjects |                   |
| Table View Cell | Value              | Table Cell View |                 | objectValue.key   |
| (Key column)    |                    |                 |                 |                   |
| Table View Cell | Value              | Table Cell View |                 | objectValue.value |
| (Value column)  |                    |                 |                 |                   |

(不要混淆Table View Cell和Table Cell View。可怕的命名,我知道)

结果

带有Cocoa绑定的NSOutline视图

在这两种方法中,您都可以使用DateFormatter来获得更好的日期输出,但这对于本问题并非必要。


1
嘿,非常感谢!我会投入一些时间来理解所有这些,迄今为止感谢你的帮助!;-) - j3141592653589793238
1
你需要将这个内容转化为一篇博客文章,并建立自己的博客——名为“瘦子教程”——这太棒了。感谢你的付出。我现在只是卡在如何使大纲视图可选择上——即为层次结构的每个级别添加一个复选框。 - UKDataGeek
1
@MobileBloke 谢谢夸奖。你可以根据你目前的进展提出一个新问题。自回答这个问题以来,我已经对我的技术进行了一些改进。 - Code Different
太棒了 - 我在这里分享了我的方法 - https://stackoverflow.com/questions/51484452/building-an-outline-view-with-check-marks - UKDataGeek
奇怪:如果我直接在应用程序中使用它,它可以完美地工作。但是如果我从故事板加载视图控制器,则无法扩展。有任何想法为什么? - mmm
显示剩余5条评论

0

这是一个非常清晰的例子,非常适合初学者使用NSOutlineView。
由于我使用的是较新版本的Swift,所以我不得不更改

switch (columnIdentifier, item)

switch (columnIdentifier.rawValue, item)

Interface Builder也会自动进行正确的调整,以设置

let cell = outlineView.make(withIdentifier: "outlineViewCell", owner: self) as! NSTableCellView

let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "outlineViewCell"), owner: self) as! NSTableCellView


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接