Filtering NSTable while typing into NSTextField - auto-select first row - swift

I have a NSTextView field which filters a NSTable table as user types in the input. I have successfully implemented table filtering.
Now, my goal is to auto-select the first result (the first row in the table) and allow user to use arrow keys to move between the results while typing the search query. When moving between the results in the table, the input field should stay focused. (This is similar to how Spotlight works).
This is how the app looks now:
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"),
];
}
}
Thank you

This kinda, sorta has an answer already in the duplicate posted by #Willeke, but 1) that answer is in Objective-C, not Swift, 2) I can provide a somewhat more detailed answer (with pictures!), and 3) I'm brazenly going after the bounty (Rule of Acquisition #110). So, with that in mind, here's how I'd implement what you're trying to do:
Don't use an NSTextView; use an NSTextField, or even better, an NSSearchField. NSSearchField is great because we can set it up in Interface Builder to create the filter predicate with almost no code. All we have to do to do that is to create an NSPredicate property in our view controller, and then set up the search field's Bindings Inspector to point to it:
Then you can create an Array Controller, with its Filter Predicate bound to that same property, and its Content Array binding bound to a property on the view controller:
And, of course, bind the table view to the Array Controller:
Last but not least, bind the text field in your table's cell view to the title property:
With all that set up in Interface Builder, we hardly need any code. All we need is the definition of the Project class (all properties need to be marked #objc so that the Cocoa Bindings system can see them):
class Project: NSObject {
#objc let title: String
init(title: String) {
self.title = title
super.init()
}
}
We also need properties on our view controller for the projects, array controller, and filter predicate. The filter predicate needs to be dynamic so that Cocoa Bindings can be notified when it changes and update the UI. If projects can change, make that dynamic too so that any changes to it will be reflected in the UI (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 on our view controller conforming it to NSSearchFieldDelegate (or NSTextFieldDelegate if you're using an NSTextField instead of an NSSearchField), on which we'll implement the control(:textView:doCommandBy:) method. Basically we intercept text-editing commands being performed by the search field's field editor, and if we get moveUp: or moveDown:, return true to tell the field editor that we will be handling those commands instead. For everything other than those two selectors, return false to tell the field editor to do what it'd normally do.
Note that this is the reason that you should use an NSTextField or NSSearchField rather than an NSTextView; this delegate method will only be called for NSControl subclasses, which NSTextView is 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
}
}
}
Voilà!
(Of course, if you prefer to populate the table view manually instead of using bindings, you can ignore most of this and just implement control(:textView:doCommandBy:), updating your table's selection manually instead of asking your array controller to do it. Using bindings, of course, results in nice, clean code, which is why I prefer it.)

As #Willeke points out, this is likely a duplicate. The solution from that other question works here. I've converted it to swift and added some explanation.
I tested this with an NSSearchField instead of an NSTextField, but I expect it should 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've already set the text field's delegate to the ViewController, so you're all set there.
This will cause your NSTextField to first check the delegate before executing the moveUp(_:) selector (triggered by pressing the up arrow). Here, the function responds saying "don't do what you normally do, the delegate will handle it" (by returning true) and sends the event to the NSTableView object instead. Focus is not lost on the text field.

Related

NSTableView not appearing at all

I've just started working on my first project for macOS and am having trouble setting up a NSTableView. When I run it the window will appear but there is nothing in it. I've made sure all the objects have the correct class in the identity inspector and can't seem to find what I'm doing wrong.
The goal of the app is to make a notes app. I want a tableView which displays the titles of all the notes in the database, in a single column, so when you click on the cell the note will then be displayed in the rest of the window.
Here's the code:
import Foundation
import AppKit
import SQLite
class NoteCloudVC: NSViewController {
// Declare an array of Note objects for populating the table view
var notesArray: [Note] = []
// IBOutlets
#IBOutlet weak var tableView: NSTableView!
// ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
// set the tableViews delegate and dataSource to self
tableView.delegate = self
tableView.dataSource = self
//Establsih R/W connection to the db
do {
let path = NSSearchPathForDirectoriesInDomains(
.applicationSupportDirectory, .userDomainMask, true
).first! + "/" + Bundle.main.bundleIdentifier!
// create parent directory iff it doesn’t exist
try FileManager.default.createDirectory(
atPath: path,
withIntermediateDirectories: true,
attributes: nil
)
let db = try Connection("\(path)/db.sqlite3")
//Define the Notes Table and its Columns
let notes = Table("Notes")
let id = Expression<Int64>("ID")
let title = Expression<String>("Title")
let body = Expression<String>("Body")
/*
Query the data from NotesAppDB.sqlite3 into an array of Note objs
Then use that array to populate the NSTableView
*/
for note in try db.prepare(notes) {
let noteToAdd = Note(Int(note[id]), note[title], note[body])
notesArray.append(noteToAdd)
}
} catch {
print(error)
}
}
// viewWillAppear
override func viewWillAppear() {
super.viewWillAppear()
tableView.reloadData()
}
}
// NSTableViewDataSource Extension of the NoteCloudVC
extension NoteCloudVC: NSTableViewDataSource {
// Number of rows ~ returns notesArray.count
func numberOfRows(in tableView: NSTableView) -> Int {
return notesArray.count
}
}
// NSTableViewDelegate extension of the NoteCloudVC
extension NoteCloudVC: NSTableViewDelegate {
// Configures each cell to display the title of its corresponding note
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
//configure the cell
if tableColumn?.identifier == NSUserInterfaceItemIdentifier(rawValue: "NotesColumn") {
let cellIdentifier = NSUserInterfaceItemIdentifier(rawValue: "NotesCell")
guard let noteCell = tableView.makeView(withIdentifier: cellIdentifier, owner: self) as? NotesCell else { return nil }
let note = notesArray[row]
noteCell.noteTitle.stringValue = note.title
return noteCell
}
return nil
}
}
// NotesCell class
class NotesCell: NSTableCellView {
// IBOutlet for the title
#IBOutlet weak var noteTitle: NSTextField!
}
I'm pretty familiar with UIKit so I thought the learning curve of AppKit would be a little better than SwiftUI, so if anyone could provide some guidance about where I've gone wrong that would be very much appreciated. Also if it will be a better use of my time to turn towards SwiftUI please lmk.
Here's the values while debugging:
It's reading the values from the table correctly, so I've at least I know the problem lies somewhere in the tableView functions.
The most confusing part is the fact that the header doesn't even show up. This is all I see when I run it:
Here are some images of my storyboard as well:
This is for an assignment for my software modeling and design class where my professor literally doesn't teach anything. So I'm very thankful for everyone who helps with this issue because y'all are basically my "professors" for this class. When I move the tableView to the center of the view controller in the story board I can see a little dash for the far right edge of the column but that's it, and I can't progress any further without this tableView because the whole app is dependant upon it.
So, it turns out that the code itself wasn't actually the problem. I had always used basic swift files when writing stuff for iOS so it never occured to me that I'd need to import Cocoa to use AppKit but that's where the problem lied all along. Using this code inside the auto-generated ViewController class that had Cocoa imported did the trick. Also I got rid of the extensions and just did all the Delegate/ DataSource func's inside the viewController class.

Prevent NSToolbarItem from being removed

I want to prevent certain toolbar items from being removed by the user. They should still be movable, just not removable.
I tried creating a custom subclass of NSToolbar with a custom removeItem(at:) implementation, but it seems this method is not even called if the user drags an item out of the toolbar in the customization palette.
The delegate also doesn't seem to expose functionality for this.
How can I disable removal of certain NSToolbarItems?
I am not sure if you can prevent it from being removed but you can implement the optional toolbarDidRemoveItem method and insert the item that you don't want it to be removed back:
import Cocoa
class WindowController: NSWindowController, NSToolbarDelegate {
#IBOutlet weak var toolbar: Toolbar!
override func windowDidLoad() {
super.windowDidLoad()
toolbar.delegate = self
}
func toolbarDidRemoveItem(_ notification: Notification) {
if let itemIdentifier = (notification.userInfo?["item"] as? NSToolbarItem)?.itemIdentifier,
itemIdentifier.rawValue == "NSToolbarShowColorsItem" {
toolbar.insertItem(withItemIdentifier: itemIdentifier, at: 0)
}
}
}
Since it is not super critical if they are removed in case a private API call would stop working, I opted for the private API solution.
extension NSToolbarItem {
func setIsUserRemovable(_ flag: Bool) {
let selector = Selector(("_setIsUserRemovable:"))
if responds(to: selector) {
perform(selector, with: flag)
}
}
}
This works exactly as advertised.

Private IBOutlets Swift

I know that our IBOutlets should be private, but for example if I have IBOutlets in TableViewCell, how should I access them from another ViewController? Here is the example why I'm asking this kind of question:
class BookTableViewCell: UITableViewCell {
#IBOutlet weak private var bookTitle: UILabel!
}
if I assign to the IBOutlet that it should be private, I got an error in another ViewController while I'm accessing the cell property: 'bookTitle' is inaccessible due to 'private' protection level
If I understand your question correctly, you are supposing the #IBOutlet properties should be marked as private all the time... Well it's not true. But also accessing the properties directly is not safe at all. You see the ViewControllers, TableViewCells and these objects use Implicit unwrapping on optional IBOutlets for reason... You don't need to init ViewController when using storyboards or just when using them somewhere in code... The other way - just imagine you are creating VC programmatically and you are passing all the labels to the initializer... It would blow your head... Instead of this, you come with this in storyboard:
#IBOutlet var myLabel: UILabel!
this is cool, you don't need to have that on init, it will just be there waiting to be set somewhere before accessing it's value... Interface builder will handle for you the initialization just before ViewDidLoad, so the label won't be nil after that time... again before AwakeFromNib method goes in the UITableViewCell subclass, when you would try to access your bookTitle label property, it would crash since it would be nil... This is the tricky part about why this should be private... Otherwise when you know that the VC is 100% on the scene allocated there's no need to be shy and make everything private...
When you for example work in prepare(for segue:) method, you SHOULD NEVER ACCESS THE #IBOutlets. Since they are not allocated and even if they were, they would get overwritten by some internal calls in push/present/ whatever functions...
Okay that's cool.. so what to do now?
When using UITableViewCell subclass, you can safely access the IBOutlets (ONLY IF YOU USE STORYBOARD AND THE CELL IS WITHIN YOUR TABLEVIEW❗️)
and change their values... you see
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// We shouldn't return just some constructor with UITableViewCell, but who cares for this purposes...
guard let cell = tableView.dequeueReusableCell(withIdentifier: "bookTableViewCell", for: indexPath) else { return UITableViewCell() }
cell.bookTitle.text = "any given text" // This should work ok because of interface builder...
}
The above case should work in MVC pattern, not MVVM or other patterns where you don't use storyboards with tableViewControllers and embed cells too much... (because of registering cells, but that's other article...)
I will give you few pointers, how you can setup the values in the cell/ViewController without touching the actual values and make this safe... Also good practice (safety) is to make the IBOutlets optional to be 100% Safe, but it's not necessary and honestly it would be strange approach to this problem:
ViewControllers:
class SomeVC: UIViewController {
// This solution should be effective when those labels could be marked weak too...
// Always access weak variables NOT DIRECTLY but with safe unwrap...
#IBOutlet var titleLabel: UILabel?
#IBOutlet var subtitleLabel: UILabel?
var myCustomTitle: String?
var myCustomSubtitle: String?
func setup(with dataSource: SomeVCDataSource ) {
guard let titleLabel = titleLabel, let subtitleLabel = subtitleLabel else { return }
// Now the values are safely unwrapped and nothing can crash...
titleLabel.text = dataSource.title
subtitleLabel.text = dataSource.subtitle
}
// WHen using prepare for segue, use this:
override func viewDidLoad() {
super.viewDidLoad()
titleLabel.text = myCustomTitle
subtitleLabel.text = myCustomSubtitle
}
}
struct SomeVCDataSource {
var title: String
var subtitle: String
}
The next problem could be this:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destinationVC = segue.destination as? SomeVC else { return }
let datasource = SomeVCDataSource(title: "Foo", subtitle: "Bar")
// This sets up cool labels... but the labels are Nil before the segue occurs and even after that, so the guard in setup(with dataSource:) will fail and return...
destinationVC.setup(with: datasource)
// So instead of this you should set the properties myCustomTitle and myCustomSubtitle to values you want and then in viewDidLoad set the values
destinationVC.myCustomTitle = "Foo"
destinationVC.myCustomSubtitle = "Bar"
}
You see, you don' need to set your IBOutlets to private since you never know how you will use them If you need any more examples or something is not clear to you, ask as you want... Wish you happy coding and deep learning!
You should expose only what you need.
For example you can set and get only the text property in the cell.
class BookTableViewCell: UITableViewCell {
#IBOutlet weak private var bookTitleLabel: UILabel!
var bookTitle: String? {
set {
bookTitleLabel.text = newValue
}
get {
return bookTitleLabel.text
}
}
}
And then, wherever you need:
cell.bookTitle = "It"
Now outer objects do not have access to bookTitleLabel but are able to change it's text content.
What i usually do is configure method which receives data object and privately sets all it's outlets features.
I haven't come across making IBOutlets private to be common, for cells at least. If you want to do so, provide a configure method within your cell that is not private, which you can pass values to, that you want to assign to your outlets. The function within your cell could look like this:
func configure(with bookTitle: String) {
bookTitle.text = bookTitle
}
EDIT: Such a function can be useful for the future, when you change your cell and add new outlets. You can then add parameters to your configure function to handle those. You will get compiler errors everywhere, where you use that function, which allows you to setup your cell correctly wherever you use it. That is helpful in a big project that reuses cells in different places.

textDidChange points to the wrong NSCollectionViewItem

I am trying to build an NSCollectionView filled with multiple editable TextViews. (OS X app in Swift.) My subclass of NSCollectionViewItem is called NoteViewItem. I am trying to have the program detect when one of the TextView has changed. I tried using both controlTextDidChange and textDidChange in the NoteViewItem's delegate with test print statement to see which would work. ControlTextDidChange did nothing; textDidChange recognized a change happened, so I went with that.
The problem is that textDidChange appears to point to a different NoteViewItem than the one that was shown on screen in the first place. It wasn't able to recognize the variable (called theNote) set in the original NoteViewItem; when I ask NoteViewItem to print String(self), I get two different results, one while setting the initial text and one in textDidChange. I'm wondering if I've set up my delegates and outlets wrongly. Any thoughts on why my references are off here?
Here's my code for NoteViewItem:
import Cocoa
class NoteViewItem: NSCollectionViewItem, NSTextViewDelegate
{
// MARK: Variables
#IBOutlet weak var theLabel: NSTextField!
#IBOutlet var theTextView: NSTextView!
var theNote: Note?
{
didSet
{
// Pre: The NoteViewItem's theNote property is set.
// Post: This observer has set the content of the *item's text view*, and label if it has one.
guard viewLoaded else { return }
if let theNote = theNote
{
// textField?.stringValue = theNote.noteText
theLabel.stringValue = theNote.filename
theTextView.string = theNote.noteText
theTextView.display()
print("theTextView.string set to "+theTextView.string!+" in NoteViewItem "+String(self))
}
else
{
theLabel.stringValue = "Empty note?"
}
}
}
// MARK: Functions
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
// Hopefully this will set the note's background to white.
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.whiteColor().CGColor
}
// MARK: - NSTextViewDelegate
/*
override func controlTextDidChange(notification: NSNotification)
{
print("Control text changed.")
}
*/
func textDidChange(notification: NSNotification)
{
if let noteyMcNoteface = theNote
{
print("On edit, we have a note: "+String(noteyMcNoteface))
}
else
{
print("On edit, we have no note. I am NoteViewItem "+String(self))
}
}
}
I figured it out. My delegate, in the TextView, was connected to the wrong object in the Interface Builder for NoteViewItem.xib. I had connected it to the object labelled Note View Item, under objects in the outline. It should have been connected to File's Owner instead, since File's Owner stands for the NoteViewItem.swift class associated with the xib.
You'd think that if you want to connect the delegate to the NoteViewItem class and there is exactly one Note View Item listed in the outline, then that Note View Item is the thing you want to connect it to. Nope, you connect it to something entirely different that isn't called the Note View Item but is the Note View Item. I'm glad Interface Builder makes things so simple.

How to update search results when scope button changed. Swift UISearchController

How to update search results when scope button changed (after my click on scope)?
Search results changed (with new scope) when I type again!
searchControl - config
import UIKit
class ProductTableView: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchResultsUpdating
{
#IBOutlet weak var tableView: UITableView!
var searchController: UISearchController!
var friendsArray = [FriendItem]()
var filteredFriends = [FriendItem]()
override func viewDidLoad()
{
super.viewDidLoad()
searchController = UISearchController(searchResultsController: nil)
searchController.searchBar.sizeToFit()
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.scopeButtonTitles = ["Title","SubTitle"]
definesPresentationContext = true
tableView.tableHeaderView = searchController.searchBar
self.tableView.reloadData()
}
Update Func
When I type text NSLog print my text and scope number.
When I change scope - nothing!!!
func updateSearchResultsForSearchController(searchController: UISearchController) {
let searchText = searchController.searchBar.text
let scope = searchController.searchBar.selectedScopeButtonIndex
NSLog("searchText - \(searchText)")
NSLog("scope - \(scope)")
filterContents(searchText, scope: scope)
tableView.reloadData()
}
Filter func
func filterContents(searchText: String, scope: Int)
{
self.filteredFriends = self.friendsArray.filter({( friend : FriendItem) -> Bool in
var fieldToSearch: String?
switch (scope){
case (0):
fieldToSearch = friend.title
case(1):
fieldToSearch = friend.subtitle
default:
fieldToSearch = nil
}
var stringMatch = fieldToSearch!.lowercaseString.rangeOfString(searchText.lowercaseString)
return stringMatch != nil
})
}
Help me, please!
The behaviour you're expecting is logical and seems on the surface to be correct, but actually isn't. Thankfully, there's an easy workaround.
Here's Apple's description of the method:
Called when the search bar becomes the first responder or when the user makes changes inside the search bar.
A scope change is a change in the search bar, right? Makes sense to me. But if you read the discussion, Apple makes it clear the behaviour isn't what you expect:
This method is automatically called whenever the search bar becomes the first responder or changes are made to the text in the search bar.
Not included in that: Changes to the scope. Weird thing to overlook, isn't it? Either the method should be called when the scope is changed, or the summary should be clear it isn't.
You can get the behaviour you want by adding the UISearchBarDelegate protocol to your view controller and setting searchController.searchBar.delegate to your view controller.
Then add this:
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
updateSearchResultsForSearchController(searchController)
}
This will cause updateSearchResultsForSearchController to be fired whenever the scope changes, as you're expecting. But instead, you might want to factor the meat of updateSearchResultsForSearchController into a new method that both updateSearchResultsForSearchController and selectedScopeButtonIndexDidChange call.