cocoanstableviewnssortdescriptorswift4.2

NSTableView - Better solution for sorting collection with NSSortDescriptor


I have a NSTableView with 2 columns bound with a custom type (SelectedFiles) array as File Name and File Path, after clicking the header, I want it to sort the data in ascending / descending order, I tried these codes with NSSortDescriptor:

class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        let fileNameSortDescriptor = NSSortDescriptor(key: "fileName", ascending: true, selector: #selector(NSString.localizedStandardCompare(_:)))
        tableView.tableColumns[0].sortDescriptorPrototype = fileNameSortDescriptor
        // other codes
    }
}

extension ViewController: NSTableViewDataSource, NSTableViewDelegate {

    func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
        let selectedFilesArray = NSMutableArray(array: selectedFiles)
        selectedFilesArray.sort(using: tableView.sortDescriptors) // Signal SIGABRT
        selectedFiles = selectedFilesArray as! [SelectedFiles]
        tableView.reloadData()
    }
}

My custom collection for the data in table view:

struct SelectedFiles: CustomStringConvertible {
    let fileName: String
    let filePath: String
    var description: String {
        return "\(fileName) at path \(filePath)"
    }
}

var selectedFiles: [SelectedFiles] = []

It turns out it doesn't work at all, IDK if its anything wrong with my code or I'm missing something.

So, I came up with this awkward solution:

var tableViewSortingOrder = ComparisonResult.orderedAscending

extension ViewController: NSTableViewDataSource, NSTableViewDelegate {

    func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
        switch tableViewSortingOrder {
        case .orderedAscending:
            tableViewSortingOrder = .orderedDescending
            selectedFiles.sort { (previous, next) -> Bool in
                return previous.fileName.compare(next.fileName) == tableViewSortingOrder
            }
        default:
            tableViewSortingOrder = .orderedAscending
            selectedFiles.sort { (previous, next) -> Bool in
                return previous.fileName.compare(next.fileName) == tableViewSortingOrder
        }
        tableView.reloadData()
    }
}

After I changed to this solution, it worked perfectly as it switches swiftly between ascending / descending order. But, when it comes to deleting objects in the collection, it throws Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value when I'm trying to delete multiple objects from both collection and table view with some specific files.

So, I'm thinking if I should change a way of achieving this header sorting thing by using NSSortDescriptor (use the old-fashioned way by correcting my first method) in order to get away from this issue, I have to admit that my second way is a bit of awkward (is more like a plan C).

I've red through multiple StackOverflow posts on this topic and I tried all of their ways, especially this one, I am not using CoreData which its solutions does not work for my situation.

Anyone can help point out the way please? 😊


Solution

  • I red the guide to NSTableView from Apple Developer Site and few other StackOverflow posts, I found myself a workable solution for Swift 4:

    I set the sortDescriptorPrototype to fileNameSortDescriptor in viewDidLoad() under ViewController class.

    class ViewController: NSViewController {
        override func viewDidLoad()
            super.viewDidLoad()
            let fileNameSortDescriptor = NSSortDescriptor(key: "fileName", ascending: true, selector: #selector(NSString.localizedStandardCompare))
            let tableColumn = tableView.tableColumn(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "fileNameColumn"))!
            tableColumn.sortDescriptorPrototype = fileNameSortDescriptor
            // other codes
        }
    }
    

    And then I added an inheritance from NSObject and inserted @objcMembers to prevent warning: Object <#object#> of class '<#class#>' does not implement methodSignatureForSelector: -- trouble ahead from occurring and then cause Signal SIGABRT while calling selectedFiles.sort(using: tableView.sortDescriptors) (Reference: Object X of class Y does not implement methodSignatureForSelector in Swift).

    @objcMembers class SelectedFiles: NSObject {
        let fileName: String
        let filePath: String
        override var description: String {
            return "\(fileName) at path \(filePath)"
    
        init(fileName: String, filePath: String) {
            self.fileName = fileName
            self.filePath = filePath
        }
    }
    

    Here's the code for tableView(_:sortDescriptorsDidChange:) in NSTableViewDataSource:

    extension ViewController: NSTableViewDataSource {
        func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
            var selectedFilesArray = NSArray(array: selectedFiles)
            selectedFilesArray = selectedFilesArray.sortedArray(using: tableView.sortDescriptors) as NSArray
            selectedFiles = selectedFilesArray as! [SelectedFiles]
            tableView.reloadData()
        }
    }
    

    Now, everything works perfectly fine.