swiftmacosnstableviewnstableviewcellnstablecolumn

Trying to use NSTableView


I'm an iOS dev and I'm creating my first Mac app. Running into some difficulties when trying to use NSTableView.

extension HomeViewController:NSTableViewDataSource{
    func numberOfRows(in tableView: NSTableView) -> Int {
        print(self.customerApplicationList.count) // '1' gets printed here
        return self.customerApplicationList.count
    }

    func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?{
        var result:NSTableCellView
        result  = tableView.make(withIdentifier: "firstName", owner: self) as! NSTableCellView
        result.textField?.stringValue = "test"
        return result
    }
}

enter image description here

Why is there no cell showing up with the value "test" in there? (at run time, didn't include a screenshot of this)


Solution

  • If you put a log in your tableView(_:viewFor:row:) method, you'll find out that it's never being called. Why is this, you may wonder? Well, it's complicated:

    AppKit, as we all know, is not implemented using Swift; it's implemented in Objective-C. Objective-C, being a very dynamic language, allows a caller to query whether an object responds to a certain message, so all an object has to do is implement a method like -tableView:viewForTableColumn:row:, and through the magic of Objective-C, AppKit can find the method and call it. With Swift, things are a little more complicated because, by default, Swift methods are not exposed to Objective-C, unless we explicitly make it so via the @objc keyword, if the method is an override of an Objective-C superclass method, or if the method satisfies an Objective-C protocol. The third of these cases should occur here, except that it turns out that tableView(_:viewFor:row:) actually belongs to NSTableViewDelegate, not NSTableViewDataSource. Therefore, the Swift compiler does not see your method as satisfying any protocol, and it is left unexposed to Objective-C. So from AppKit's point of view, it's as if you didn't implement it at all.

    To solve your immediate problem, add NSTableViewDelegate to your extension, and make sure your data source is set as the delegate in Interface Builder. However, when making Mac apps, I find it easier to use Cocoa Bindings to populate table views, as you get a lot of features "for free" such as automatic sorting by column, type-ahead selection, and selection management. To do this, follow these steps:

    1) Make sure the array property on your object is marked with both the @objc and the dynamic keyword, and that the class contained within the array is an NSObject subclass, and has its relevant properties also marked @objc and dynamic:

    class Thingy: NSObject {
        @objc dynamic var name: String
    
        init(name: String) { self.name = name }
    }
    
    class MyViewControllerThingy: NSViewController {
        @objc dynamic var myArray: [Thingy] = [Thingy(name: "Foo"), Thingy(name: "Bar")]
    }
    

    This makes sure that AppKit can do its dynamic Objective-C magic to automatically make this property KVO-compliant, so we don't have to do it ourselves (this is needed because Cocoa Bindings is built on KVO).

    2) Make an Array Controller in Interface Builder, and in the Bindings Inspector, set the Array Controller's "Model Key Path" to the name of your property:

    enter image description here

    3) Now select your table view, and in its Bindings Inspector, bind its Content, Selection Indexes, and Sort Descriptors to arrangedObjects, selectionIndexes, and sortDescriptors respectively, leaving "Model Key Path" blank for each:

    enter image description here

    4) Select your text field in the table view's cell, go to its Bindings Inspector, and bind it to the table cell view, with a Model Key Path of objectValue. and then the property name that you want to view in the cell:

    enter image description here

    5) And finally, select the table column and set its Sort Key to the property name in the Attributes Inspector (the "Selector" field lets you customize what method to call on the objects to sort them; I like to use localizedStandardCompare: for strings to get case-insensitive sorting, but for most other types you can just leave this at the default):

    enter image description here

    Et voilà:

    enter image description here

    This may seem like a lot of twiddling around in Interface Builder, but at the end of it we've set up the entire table with almost no code. And, we get a really slick UI for your users, along with a bunch of features for free, my favorite being automatic sorting by clicking on the headers:

    enter image description here

    The great thing about this is not just that you don't have to bother with re-sorting the array yourself, but it doesn't even mess with the order of your original array; the change is for display purposes only. The array here is still ["Foo", "Bar"].

    Another really cool feature of NSArrayController is that it will manage the selection for you as well. If your table view is a sidebar, for example, you can bind another view on the right to the selected object in the array controller, and this way you can easily implement things like Mail.app's pane viewer, including things like specifying placeholders to use if the user selects more than one object at once. It's really slick.