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

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.

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.

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

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.

Using a UISegmentedControl like a UISwitch

Is it possible to use a UISegmentedControl with 3 segments as if it was a three-way UISwitch? I tried to use one as a currency selector in the settings section of my app with no luck, it keeps reseting to the first segment when I switch views and that creates a big mess.
I proceeded like that:
IBAction func currencySelection(_ sender: Any) {
switch segmentedControl.selectedSegmentIndex {
case 0:
WalletViewController.currencyUSD = true
WalletViewController.currencyEUR = false
WalletViewController.currencyGBP = false
MainViewController().refreshPrices()
print(0)
case 1:
WalletViewController.currencyUSD = false
WalletViewController.currencyEUR = true
WalletViewController.currencyGBP = false
MainViewController().refreshPrices()
print(1)
case 2:
WalletViewController.currencyUSD = false
WalletViewController.currencyEUR = false
WalletViewController.currencyGBP = true
MainViewController().refreshPrices()
print(2)
default:
break
}
}
The UISegmentedControl is implemented in the
SettingsViewController of the app to choose between currencies to
display in the MainViewController.
(Taken from a comment in #pacification's answer.)
This was the missing piece I was looking for. It provides a lot of context.
TL;DR;
Yes, you can use a three segment UISegmentedControl as a three-way switch. The only real requirement is that you can have only one value or state selected.
But I wasn't grasping why your code referred to two view controllers and some of switching views resulting in resetting the segment. One very good way to do what you want is to:
Have MainViewController present SettingsViewController. Presenting it modally means the user is only doing one thing at a time. When they are making setting changes, you do not want them adding new currency values.
Create a delegate protocol in SettingsViewController and make MainViewController conform to it. This tightly-couples changes made to the settings to the view controller interested in what those changes are.
Here's a template for what I'm talking about:
SettingsViewController:
protocol SettingsVCDelegate {
func currencyChanged(sender: SettingsViewController)
}
class SettingsViewController : UIViewController {
var delegate:SettingsVCDelegate! = nil
var currency:Int = 0
#IBAction func valueChanged(_ sender: UISegmentedControl) {
currency = sender.selectSegmentIndex
delegate.currencyChanged(sender:self)
}
}
MainViewController:
class MainViewController: UIViewController, SettingsVCDelegate {
var currency:Int = 0
let settingsVC = SettingsViewController()
override func viewDidLoad() {
super.viewDidLoad()
settingsVC.delegate = self
}
func presentSettings() {
present(settingsVC, animated: true, completion: nil)
}
func currencyChanged(sender:SettingsViewController) {
currency = sender.currency
}
}
You can also create an enum of type Int to make your code more readable, naming each value as currencyUSD, currencyEUR, and currencyGBP. I'll leave that to you as a learning exercise.
it keeps reseting to the first segment when I switch views
yes, it is. to avoid this situation you should set the correct switch value to the segmentedControl.selectedSegmentIndex every time when you load your view with UISegmentedControl.
UPD
Ok, the behavior of MainViewController can be similar to this:
final class MainViewController: UIViewController {
private var savedValue = 0
override func viewDidLoad() {
super.viewDidLoad()
}
func openSettingsController() {
let viewController = SettingsController.instantiate() // simplify code a bit. use the full controller initialization
viewController.configure(value: savedValue, onValueChanged: { [unowned self] value in
self.savedValue = value
})
navigationController?.pushViewController(viewController, animated: true)
}
}
And the SettingsViewController:
final class SettingsViewController: UIViewController {
#IBOutlet weak var segmentedControl: UISegmentedControl!
private var value: Int = 0
var onValueChanged: ((Int) -> Void)?
func configure(value: Int, onValueChanged: #escaping ((Int) -> Void)) {
self.value = value
self.onValueChanged = onValueChanged
}
override func viewDidLoad() {
super.viewDidLoad()
segmentedControl.selectedSegmentIndex = value
}
#IBAction func valueChanged(_ sender: UISegmentedControl) {
onValueChanged?(sender.selectedSegmentIndex)
}
}
The main idea that you should keep your selected value if you moving from SettingsViewController. For this thing you can create closure
var onValueChanged: ((Int) -> Void)?
that pass back to MainViewController the selected UISegmentedControl value. And in future when you will open the SettingsViewController again you just configure() this value and set it to UI.

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.

Swift TextField Method Not Called, Delegate is Set, now BAD_ACCESS Errors

there are many similar questions about TextFields delegate method textfieldshouldreturn not being called, but all were solved by setting the delegate. Ive set the delegate, and also have a perfectly fine example in another project I've copied almost line for line. A print statement confirms no call is made. Whats more curious is that I set a random variable to test if I was even accessing the right object, but when I tried to access that variable, it crashed with a BAD_ACCESS error.
class TitleTextField: UITextField, UITextFieldDelegate {
var randomElement: Bool = true
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
print("text field return pressed")
return true
}
}
and here is where I'm using it
class EditViewController: UIViewController {
#IBOutlet weak var titleTextField: TitleTextField!
func configureView() {
navigationItem.title = "Edit Goal"
}
override func viewDidLoad() {
super.viewDidLoad()
print("editor loaded")
configureView()
titleTextField.text = "placeholder"
titleTextField.delegate = titleTextField
titleTextField.delegate = titleTextField.self
if let textField = titleTextField {
textField.delegate = titleTextField
}
print("textfield delegate = \(titleTextField?.delegate)")
}
If listed some of the different ways I tried setting the delegate. I even conformed the viewController to UITextFieldDelegate and set the delegate to self but that didn't matter either. I added "randomVariable" to TitleTextField to make sure I was accessing the correct object, but when I used titleTextField.randomVariable = true in viewDidLoad, I got a BAD_ACCESS crash.
Ive also double checked the storyboard connection. I even deleted the connection and IBoutlet and redid them, no difference. cleaned project etc.
Wow ok, so the problem was I hadnt set the textfield class to TitleTextField in my identity inspector. I had it programmatically set, I guess I didnt realize i had to do it in the storyboard too.
The issue is that you're conforming to the UITextFieldDelegate on your custom TitleTextField itself. Instead, you should conform to the protocol on your UIViewController, like so:
class EditViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var titleTextField: TitleTextField!
func configureView() {
navigationItem.title = "Edit Goal"
}
override func viewDidLoad() {
super.viewDidLoad()
print("editor loaded")
configureView()
titleTextField.text = "placeholder"
titleTextField.delegate = self
print("textfield delegate = \(titleTextField?.delegate)")
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
print("text field return pressed")
return true
}
The purpose of the delegate is to respond to editing-related messages from the text field (link to docs). This means that the UITextField is already aware of these editing events. What you need to do is allow the class containing your custom UITextField to listen to the events that it is sending out. In your situation, that class is EditViewController. You can make EditViewController listen to the UITextView's events by setting it as the delegate.
The reason for your BAD_ACCESS error is a memory-related issue. Your UITextField is calling itself infinitely through recursion. If you look through the calling stack you'll probably see it calling the same method hundreds of times. See this post for more insight.