Search code examples
swiftmacosnstableview

Using type select on only one column with NSTableView in Swift


I'm working in XCode 9.3, Swift developing an application for MacOS.

I've created a table that currently has two columns. I want to use type select that only acts on one column so that when the user types the first few letters, it only selects within the first column and not in the 2nd column.

The Apple documentation has an example in Objective-C here: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TableView/RowSelection/RowSelection.html#//apple_ref/doc/uid/10000026i-CH6-SW1 but I can't seem to translate that into Swift. I've included the table code below. Any help at all would be greatly appreciated.

extension ViewController: NSTableViewDataSource {
  func numberOfRows(in tableView: NSTableView) -> Int {
    return directoryItems?.count ?? 0
  }

  // Sorting.
  func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
    guard let sortDescriptor = tableView.sortDescriptors.first else {
      return
    }

    if let order = Directory.FileOrder(rawValue: sortDescriptor.key!) {
      sortOrder = order
      sortAscending = sortDescriptor.ascending
      reloadFileList()
    }
  }

}

extension ViewController: NSTableViewDelegate {

  fileprivate enum CellIdentifiers {
    static let NameCell = "NameCellID"
    static let TimeCell = "TimeCellID"
    static let SizeCell = "SizeCellID"
  }

  func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {

    var image: NSImage?
    var text: String = ""
    var cellIdentifier: String = ""

    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .long
    dateFormatter.timeStyle = .long

    guard let item = directoryItems?[row] else {
      return nil
    }

    if tableColumn == tableView.tableColumns[0] {
      image = item.icon
      text = item.name
      cellIdentifier = CellIdentifiers.NameCell

    } else if tableColumn == tableView.tableColumns[1] {
      let asset = AVAsset(url: item.url as URL)
      let audioTime = asset.duration
      text = audioTime.durationText
      cellIdentifier = CellIdentifiers.TimeCell

    } else if tableColumn == tableView.tableColumns[2] {
      text = item.isFolder ? "--" : sizeFormatter.string(fromByteCount: item.size)
      cellIdentifier = CellIdentifiers.SizeCell
    }

    if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: cellIdentifier), owner: nil) as? NSTableCellView {
      cell.textField?.stringValue = text
      cell.imageView?.image = image ?? nil
      return cell
    }

    return nil
  }

}

Solution

  • Your problem is twofold:

    First, you need to set an Identifier for each of your table columns. You can do this by selecting the column in Interface Builder, going to the Identity Inspector, and setting it to some string value:

    enter image description here

    Once you've done that, create some static properties to refer to these identifiers (this isn't strictly necessary, since you could of course just use rawValue to do a plain string comparison the Objective-C way, but the new Swift 4 identifiers are type-safe, and thus preferred). Do that like this:

    extension ViewController: NSTableViewDelegate {
        private struct TableColumns {
            static let foo = NSUserInterfaceItemIdentifier("Foo")
            static let bar = NSUserInterfaceItemIdentifier("Bar")
        }
    
        ...
    
    }
    

    Now, you can use these identifiers to refer to your columns, using tableColumn(withIdentifier:) (or column(withIdentifier:) if you just want the column number). I recommend doing this everywhere you refer to them, including in your tableView(:viewFor:row:) method, since the way you're doing it now with tableView.tableColumns[0] and so forth depends on the order of the columns, and if the user reorders them, it may cause unexpected behavior. Using the identifier will make sure you're always looking at the column you think you're looking at.

    Anyway, once you've got your identifiers set up, you can address the second problem: You're using the wrong delegate method. Instead of tableView(:shouldTypeSelectFor:searchString:), which is meant for catching these things at the event level (i.e., the user just pressed a key. Should I invoke the type select system at all?), use tableView(:typeSelectStringFor:row:). This method lets you return the string given to the type selection mechanism for each row/column combination in your table. For the ones you want type-select to ignore, just return nil; otherwise, return the string that you'd expect to have to type to select that particular row. So, something like:

    func tableView(_ tableView: NSTableView, typeSelectStringFor tableColumn: NSTableColumn?, row: Int) -> String? {
        if tableColumn?.identifier == TableColumns.foo {
            return directoryItems?[row].name
        } else {
            return nil
        }
    }