I struggled the last few days with creating a filter in a Swift project for a TableViewController that uses Core Data. I finally realized that I needed to use a UISearchController , create an NSPredicate for searchController.searchBar , etc.
I found this post to be EXTREMELY useful , but after modeling my TVC after this project, I found that "all the lights but no one is home." I can search, the predicate is created in the searchBar , add, delete, etc., but the cells are not updated for the search. I'm missing something, but I donβt know what.
Here are the relevant parts of my code.
class MasterViewController: UITableViewController, NSFetchedResultsControllerDelegate, UISearchControllerDelegate, UISearchResultsUpdating // Properties that get instantiated later var detailViewController: DetailViewController? = nil var addNoteViewController:AddNoteViewController? = nil // I added this var managedObjectContext: NSManagedObjectContext? = nil // Added variable for UISearchController var searchController: UISearchController! var searchPredicate: NSPredicate? // I added this. It optional on and gets set later override func viewDidLoad() { super.viewDidLoad() self.navigationItem.leftBarButtonItem = self.editButtonItem() if let split = self.splitViewController { let controllers = split.viewControllers let context = self.fetchedResultsController.managedObjectContext let entity = self.fetchedResultsController.fetchRequest.entity! self.detailViewController = controllers[controllers.count-1].topViewController as? DetailViewController } // UISearchController setup searchController = UISearchController(searchResultsController: nil) searchController.dimsBackgroundDuringPresentation = false searchController.searchResultsUpdater = self searchController.searchBar.sizeToFit() self.tableView.tableHeaderView = searchController?.searchBar self.tableView.delegate = self self.definesPresentationContext = true } // MARK: - UISearchResultsUpdating Delegate Method // Called when the search bar text or scope has changed or when the search bar becomes first responder. func updateSearchResultsForSearchController(searchController: UISearchController) { let searchText = self.searchController?.searchBar.text // steve put breakpoint println(searchController.searchBar.text) if let searchText = searchText { searchPredicate = NSPredicate(format: "noteBody contains[c] %@", searchText) self.tableView.reloadData() println(searchPredicate) } } override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if editingStyle == .Delete { var note: Note if searchPredicate == nil { note = self.fetchedResultsController.objectAtIndexPath(indexPath) as! Note } else { let filteredObjects = self.fetchedResultsController.fetchedObjects?.filter() { return self.searchPredicate!.evaluateWithObject($0) } note = filteredObjects![indexPath.row] as! Note } let context = self.fetchedResultsController.managedObjectContext context.deleteObject(note) var error: NSError? = nil if !context.save(&error) { abort() } } } // MARK: - Fetched results controller var fetchedResultsController: NSFetchedResultsController { if _fetchedResultsController != nil { return _fetchedResultsController! } let fetchRequest = NSFetchRequest() // Edit the entity name as appropriate. let entity = NSEntityDescription.entityForName("Note", inManagedObjectContext: self.managedObjectContext!) fetchRequest.entity = entity // Set the batch size to a suitable number. fetchRequest.fetchBatchSize = 20 // Edit the sort key as appropriate. let sortDescriptor = NSSortDescriptor(key: "noteTitle", ascending: false) let sortDescriptors = [sortDescriptor] fetchRequest.sortDescriptors = [sortDescriptor] // Edit the section name key path and cache name if appropriate. // nil for section name key path means "no sections". let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: nil, cacheName: "Master") aFetchedResultsController.delegate = self _fetchedResultsController = aFetchedResultsController var error: NSError? = nil if !_fetchedResultsController!.performFetch(&error) { // Replace this implementation with code to handle the error appropriately. // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. println("Unresolved error \(error), \(error?.userInfo)") abort() } return _fetchedResultsController! } var _fetchedResultsController: NSFetchedResultsController? = nil func controllerWillChangeContent(controller: NSFetchedResultsController) { // self.tableView.beginUpdates() // ** original code, change if doesn't work** steve put breakpoint here // ANSWER said this section is redundant, but keeping it b/c it doesn't crash if searchPredicate == nil { tableView.beginUpdates() } else { (searchController.searchResultsUpdater as! MasterViewController).tableView.beginUpdates() } } func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) { var tableView = UITableView() if searchPredicate == nil { tableView = self.tableView } else { tableView = (searchController.searchResultsUpdater as! MasterViewController).tableView } switch type { case .Insert: self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade) case .Delete: self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade) default: return } }
I think my problem "lives" here in this section. Autocomplete was my friend here, but I don't see the " searchIndex " referenced by autocomplete. I think something is missing, but I'm not sure what and how.
If you did it like that, thanks for reading. Here's the GitHub repository for the branch I'm working on.
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) { var tableView = UITableView() if self.searchPredicate == nil { tableView = self.tableView } else { tableView = (self.searchController.searchResultsUpdater as! MasterViewController).tableView } switch type { case .Insert: println("*** NSFetchedResultsChangeInsert (object)") tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade) case .Delete: println("*** NSFetchedResultsChangeDelete (object)") tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade) case .Update: println("*** NSFetchedResultsChangeUpdate (object)")
Edit: Per @pbasdf, I am adding TableView methods.
// MARK: - Table View override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return self.fetchedResultsController.sections?.count ?? 0 } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if self.searchPredicate == nil { let sectionInfo = self.fetchedResultsController.sections![section] as! NSFetchedResultsSectionInfo return sectionInfo.numberOfObjects } let sectionInfo = self.fetchedResultsController.sections![section] as! NSFetchedResultsSectionInfo return sectionInfo.numberOfObjects } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell self.configureCell(cell, atIndexPath: indexPath) return cell } override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { // Return false if you do not want the specified item to be editable. return true }