So I've created an NSOutlineView
to display the file & directory list in a hierarchical way. I'm building a BitTorrent client (stating so the class names make sense).
As you can see, this is pretty much how the outline view looks:
The problem is associated with the Name column. In the name column, for each row, I have a checkbox and a text field side by side. This will help you get a clearer idea:
Now, I use bindings to get the value for each textfield. However, since there are 2 views (checkbox and textfield) that needs to bound to the same NSTableCellView
, I'm returning a struct, from the data source, containing 2 values: a string for the text field (which holds the file/directory name), and a boolean for enabling/disabling the checkbox.
To handle the outline view (especially its data), I've set its class to TorrentContent
, which is defined as below:
import Cocoa
struct Name {
let value: String
let enabled: Bool
}
class TorrentContent: NSOutlineView, NSOutlineViewDelegate, NSOutlineViewDataSource {
var content: [TorrentContentItem]
required init?(coder: NSCoder) {
let srcDir = TorrentContentItem("src")
let mainJava = TorrentContentItem("main.java")
let mainCpp = TorrentContentItem("main.cpp")
srcDir.children.append(mainJava)
srcDir.children.append(mainCpp)
content = [srcDir]
super.init(coder: coder)
delegate = self
dataSource = self
}
func outlineView(_: NSOutlineView, isItemExpandable item: Any) -> Bool {
if let _item = item as? TorrentContentItem {
if _item.children.count > 0 {
return true
} else {
return false
}
} else {
return false
}
}
func outlineView(_: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if item == nil {
return content.count
} else {
if let _item = item as? TorrentContentItem {
return _item.children.count
}
}
return 0
}
func outlineView(_: NSOutlineView, child: Int, ofItem item: Any?) -> Any {
if item != nil {
if let _item = item as? TorrentContentItem {
return _item.children[child]
}
}
return content[child]
}
func outlineView(_: NSOutlineView, objectValueFor col: NSTableColumn?, byItem item: Any?) -> Any? {
if item != nil, col != nil {
if let _item = item as? TorrentContentItem {
switch col!.title {
case "Name":
return Name(value: _item.name, enabled: false)
default:
return nil
}
}
}
return nil
}
}
I've hard-coded the data so it'll be easier for you to understand what's going on.
Focusing only on the name column, here's the part of the above code which deals with that:
func outlineView(_: NSOutlineView, objectValueFor col: NSTableColumn?, byItem item: Any?) -> Any? {
if item != nil, col != nil {
if let _item = item as? TorrentContentItem {
switch col!.title {
case "Name":
return Name(value: _item.name, enabled: false)
default:
return nil
}
}
}
return nil
}
As you can see, it returns the Name
struct, which contains values for both the views. I've hard-coded the enabled value to false just for testing purposes.
Now to bind that to the textfield's value property, I've done this:
My logic is that, since objectValue is an instance of the Name
struct, objectValue.value should be the value of the Name
struct's instance, which is a string.
I want to bind the enabled property of the checkbox in a similar way. However, none of the bindings work. They cause the app to crash. This is what XCode shows me after it crashes everytime I attempt to view the outline view during runtime:
Only got "(lldb)" in the console.
What am I doing wrong, and how do I achieve what I want? That is, setting the property values of multiple views from the data source class.
Cocoa Bindings uses Key Value Observing (KVO) and the observed object must be KVO compatible. See Using Key-Value Observing in Swift.
You can only use key-value observing with classes that inherit from NSObject.
Mark properties that you want to observe through key-value observing with both the @objc attribute and the dynamic modifier.
Solution A: Return a KVO compatble object from outlineView(_:objectValueFor:byItem:)
Solution B: Don't use Cocoa Bindings. Create a subclass of NSTableCellView
and add a enabledCheckbox
outlet. Set the values in outlineView(_:viewFor:item:)
.