NSTable filtering when entering in NSTextField - automatic selection of the first line

I have an NSTextView field that filters the NSTable table as user types in the input. I successfully performed table filtering.

Now my goal is to automatically select the first result (the first row in the table) and allow the user to use the arrow keys to move between the results when typing a search query. When moving between the results in the table, the input field should remain focused. (This is similar to how Spotlight works).

Here's what the app looks like:

attachment

This is my ViewController :

 import Cocoa class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate, NSTextFieldDelegate { @IBOutlet weak var field: NSTextField! @IBOutlet weak var table: NSTableView! var projects: [Project] = [] override func viewDidLoad() { super.viewDidLoad() projects = Project.all() field.delegate = self table.dataSource = self table.delegate = self } override func controlTextDidChange(_ obj: Notification) { let query = (obj.object as! NSTextField).stringValue projects = Project.all().filter { $0.title.contains(query) } table.reloadData() } func numberOfRows(in tableView: NSTableView) -> Int { return projects.count } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "FirstCell"), owner: nil) as? NSTableCellView { cell.textField?.stringValue = projects[row].title return cell } return nil } } 

and this is Project class

 struct Project { var title: String = "" static func all() -> [Project] { return [ Project(title: "first project"), Project(title: "second project"), Project(title: "third project"), Project(title: "fourth project"), ]; } } 

thanks

+7
cocoa swift nstextfield nstableview macos
source share
2 answers

This view, sorta has the answer already in the duplicate sent by @Willeke, but 1) that the answer is in Objective-C, and not in Swift, 2). I can give a slightly more detailed answer (with pictures!) And 3) I shamelessly go after generosity (Acquisition Rule No. 110). So, keeping in mind, this is how I implement what you are trying to do:

Do not use NSTextView ; use an NSTextField , or even better, an NSSearchField . NSSearchField excellent because we can configure it in Interface Builder to create a filter predicate with almost no code. All we need to do is create an NSPredicate property in our view controller, and then configure the Bindings Inspector search field to point to it:

enter image description here

You can then create an Array Controller with its Predicate filter bound to the same property and a Content Array binding bound to the property on the view controller:

enter image description here

And, of course, bind the table view to the array controller:

enter image description here

And last but not least, attach the text box in the table cell view to the title property:

enter image description here

When using everything that is configured in Interface Builder, we hardly need any code. All we need is a definition of the Project class (all properties must be marked with @objc so that Cocoa Bindings can see them):

 class Project: NSObject { @objc let title: String init(title: String) { self.title = title super.init() } } 

We also need properties on our project view controller, array controller, and filter predicate. The filter predicate must be dynamic , so Cocoa Bindings can be notified when the user interface changes and updates. If projects can change, make it dynamic too, so that any changes in it are reflected in the user interface (otherwise you can get rid of dynamic and just make it @objc let ).

 class ViewController: NSViewController { @IBOutlet var arrayController: NSArrayController! @objc dynamic var projects = [ Project(title: "Foo"), Project(title: "Bar"), Project(title: "Baz"), Project(title: "Qux") ] @objc dynamic var filterPredicate: NSPredicate? = nil } 

And last but not least, an extension of our view controller, corresponding to its NSSearchFieldDelegate (or NSTextFieldDelegate , if you use NSTextField instead of NSSearchField ), on which we will implement the control(:textView:doCommandBy:) . Basically, we intercept text editing commands executed by the search field field editor, and if we get moveUp: or moveDown: return true to tell the field editor that we will process these commands. For all but these two selectors, return false to tell the field editor to do what it usually does.

Note that this is the reason you should use NSTextField or NSSearchField rather than NSTextView ; this delegate method will only be called for subclasses of NSControl , which NSTextView not.

 extension ViewController: NSSearchFieldDelegate { func control(_: NSControl, textView _: NSTextView, doCommandBy selector: Selector) -> Bool { switch selector { case #selector(NSResponder.moveUp(_:)): self.arrayController.selectPrevious(self) return true case #selector(NSResponder.moveDown(_:)): self.arrayController.selectNext(self) return true default: return false } } } 

Voila!

(Of course, if you prefer to fill out the table view manually instead of using bindings, you can ignore most of this and simply implement control(:textView:doCommandBy:) by updating the table selection manually, instead of asking your array controller to do this. Bindings, of course lead to nice, clean code, so I prefer it.)

+4
source share

As @Willeke points out, this is probably a duplicate. The solution to this issue works here. I converted it to quick and added some explanation.

I tested this with NSSearchField instead of NSTextField , but I expect it to work the same.

First you need to add the NSControlTextEditingDelegate protocol to your ViewController and add the following function:

 func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { if commandSelector == #selector(moveUp(_:)) { table.keyDown(with: NSApp.currentEvent!) return true } else if commandSelector == #selector(moveDown(_:)) { table.keyDown(with: NSApp.currentEvent!) return true } return false } 

You have already set the text field delegate to the ViewController, so you are all set up there.

This will cause your NSTextField first check the delegate before executing the moveUp(_:) selector (triggered by pressing the up arrow). Here the function replies: “Do not do what you usually do, the delegate will process” (returning true ) and instead dispatches the event to the NSTableView object. Focus is not lost in the text box.

+1
source share

All Articles