Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Using NSFetchedResultsController and UISearchBarDelegate

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?

like image 475
dyatesupnorth Avatar asked Dec 14 '22 12:12

dyatesupnorth


2 Answers

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.

like image 76
Mundi Avatar answered May 04 '23 00:05

Mundi


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.

like image 35
zeeshan Avatar answered May 03 '23 23:05

zeeshan