Search code examples
swiftxcodemacosxcode11appkit

App Crashes Due to Binding to Table Cell View


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:

enter image description here

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:

enter image description here

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:

enter image description here

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:

enter image description here

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.


Solution

  • 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:).