I am looking for a decent solution to this problem. I am wanting to implement some simple search functionality on a TableView that I have.
All the examples I have found either use the deprecated UISearchDisplayController
or use the new UISearchController
but without NSFetchedResultsController
Currently this is populated using Core Data
/ NSFetchedResultsController
So far I have managed to get it to a point where I can gather the users' search string (woo!). I am aware that I may need a separate FRC
to perform the search on, but as mentioned above all attempts up to now have failed.
My class is conforming to the following protocols:
class JobListController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate, UISearchBarDelegate{
I can't use UITableViewController
as I have already written loads of existing functionality that relies on this class being a UIViewController
I have my two IBOutlets
:
@IBOutlet var tblJobs : UITableView!
@IBOutlet weak var searchBar: UISearchBar!
and my empty arrays to hold my various Core Data
bits and bobs:
var workItems = [Work]()
var filteredWorkItems = [Work]()
Here is how I am initialising my FRC
, along with my MOC
and I've left in my empty second FRC
as I am quite sure it will be needed at some point:
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
lazy var fetchedResultsController: NSFetchedResultsController = {
let workFetchRequest = NSFetchRequest(entityName: "Work")
let primarySortDescriptor = NSSortDescriptor(key: "createdDate", ascending: true)
let secondarySortDescriptor = NSSortDescriptor(key: "town", ascending: true)
workFetchRequest.sortDescriptors = [primarySortDescriptor, secondarySortDescriptor]
let frc = NSFetchedResultsController(
fetchRequest: workFetchRequest,
managedObjectContext: self.managedObjectContext!,
sectionNameKeyPath: "createdDate",
cacheName: nil)
frc.delegate = self
return frc
}()
var searchResultsController: NSFetchedResultsController?
In my viewDidLoad
function I am setting up the delegates / data source for my table and the searchBar:
tblJobs.delegate = self
tblJobs.dataSource = self
searchBar.delegate = self
and here is the searchBar
function which is where I am up to. The stringMatch
variable is leftover from a previous attempt, I am hoping to be able to search by a multitude of different parameters here, but if I can get just one working it will be a solid start.
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
println("Search text is \(searchText)")
self.filteredWorkItems = self.workItems.filter({( work: Work) -> Bool in
//
let stringMatch = work.postcode.rangeOfString(searchText)
return stringMatch != nil
})
if(filteredWorkItems.count == 0){
searchActive = false;
} else {
searchActive = true;
}
self.tblJobs.reloadData()
}
Here is my cellForRowAtIndexPath
function to show how I am pulling data from the fetchedResultsController
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cell = self.tblJobs.dequeueReusableCellWithIdentifier(
"JobCell", forIndexPath: indexPath)
as! JobTableViewCell
let workItem = fetchedResultsController.objectAtIndexPath(indexPath) as! Work
//...
return cell
}
So you can see I've got a few things going on here, ultimately I am wanting to figure out how I use my newly gotten searchText
string to query against my FRC
, and then for the results to filter properly in the View.
Update:
I have attempted to add the search string to my NSPredicate
in the FRC
like so:
lazy var fetchedResultsController: NSFetchedResultsController = {
../
workFetchRequest.predicate = NSPredicate(format:"title contains[cd] %@", savedSearchTerm!)
//...
return frc
}()
Which results in 'JobListController.Type' does not have a member named 'savedSearchTerm'
At the top of my class I have set it up like this:
var savedSearchTerm: NSString?
So not sure what I'm doing wrong?
From your code, I assume you want to use the same table view to display the results. So you just need to update your FRC with a new filter based on the search term.
Store the search term in a variable. In the FRC factory function, include the predicate, something like this:
request.predicate = searchText?.characters.count > 0 ?
NSPredicate(format:"title contains[cd] %@", searchText!) : nil
When the text changes, reset the FRC and reload.
fetchedResultsController = nil
tableView.reloadData()
If you have additional filters, such as scope buttons, add additional terms to the predicate.
Swift 4.2
This is a working solutions from one of my app. I have trimmed it down to make it simple to show how it works.
I have a database of around 6000 rows in which I search via 3 different scopes: Notes, Author and Keywords. I call initializeFetchedResultsController
in the viewDidLoad
function with default values. And later when user starts typing in the Search field, start calling it again with the required value.
The Fetch part:
let EMPTY_STRING = "" // I don't like string literals in my code so define them as static variables separately
// Giving two default values
func initializeFetchedResultsController(_ text: String: EMPTY_STRING, _ scope: Int = 0) {
fetchRequest.sortDescriptors = [NSSortDescriptor(key: NotesAttributes.author.rawValue, ascending: true)]
if searchedStringTemp != EMPTY_STRING { // Whatever conditions you want to pass on
let p0 = NSPredicate(format: NotesAttributes.scope.rawValue + " != \(scope)")
let p1 = NSPredicate(format: "\(column) CONTAINS[cd] %@", "\(text)")
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [p0, p1])
}
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: AppDelegate().sharedInstance().persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self
do {
try self.fetchedResultsController.performFetch()
} catch {
let fetchError = error as NSError
print("Error 12312: Unable to Perform Fetch Request")
print("\(fetchError), \(fetchError.localizedDescription)")
}
}
The search controller:
// MARK: - UISearchBar Delegate
extension AllNotesVC: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
// This is my function which I call to start search
notesSearched(searchBar.text!, searchBar.scopeButtonTitles![selectedScope])
}
}
// MARK: - UISearchResultsUpdating Delegate
extension AllNotesVC: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
let text = searchController.searchBar.text!
let searchBar = searchController.searchBar
let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex]
// This is my function which I call to start search
notesSearched(text, scope)
}
}
And this in my notesSearched
method which re-initialize the fetch results controller and reload the table every time.
// MARK: - Private instance methods
private func notesSearched(_ text: String, _ scope: Int) {
initializeFetchedResultsController(text, scope)
tableView.reloadData()
}
While calling doing so many table reloads might not be the most efficient way to do this, but it is lightening fast, and since this updates the table in real-time as user is typing it provides a wonderful user experience.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With