Reputation: 1117
I'm trying to implement a search for a standard Master-Detail template with the Core Data.
The search bar is working, it changes the search template (I convert the string to a date interval), but tableView.reloadData() doesn't update the table. It seems that fetchedResultsController never updates.
Here is the code:
// MasterViewController.swift
// CoreData example
import UIKit
import CoreData
class MasterViewController: UITableViewController, NSFetchedResultsControllerDelegate {
var detailViewController: DetailViewController? = nil
var managedObjectContext: NSManagedObjectContext? = nil
//--- Added to start Master-Detail project with Core Data
var taskPredicate: NSPredicate?
var searchtemplate: String? {didSet {print (searchtemplate as Any)}}
let searchController = UISearchController(searchResultsController: nil)
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
searchtemplate = searchText
tableView.reloadData()
}
//---//
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
navigationItem.leftBarButtonItem = editButtonItem
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:)))
navigationItem.rightBarButtonItem = addButton
if let split = splitViewController {
let controllers = split.viewControllers
detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
// Setup the Search Controller
searchController.searchResultsUpdater = self
searchController.searchBar.delegate = self
definesPresentationContext = true
searchController.dimsBackgroundDuringPresentation = false
// Setup the Scope Bar
searchController.searchBar.scopeButtonTitles = ["All", "...", "...", "..."]
tableView.tableHeaderView = searchController.searchBar
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed
super.viewWillAppear(animated)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func insertNewObject(_ sender: Any) {
let context = self.fetchedResultsController.managedObjectContext
let newEvent = Event(context: context)
// If appropriate, configure the new managed object.
newEvent.timestamp = NSDate()
// Save the context.
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() 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.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
// MARK: - Segues
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
let object = fetchedResultsController.object(at: indexPath)
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return fetchedResultsController.sections?.count ?? 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionInfo = fetchedResultsController.sections![section]
return sectionInfo.numberOfObjects
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let event = fetchedResultsController.object(at: indexPath)
configureCell(cell, withEvent: event)
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let context = fetchedResultsController.managedObjectContext
context.delete(fetchedResultsController.object(at: indexPath))
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() 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.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
func configureCell(_ cell: UITableViewCell, withEvent event: Event) {
cell.textLabel!.text = event.timestamp!.description
}
// MARK: - Fetched results controller
var fetchedResultsController: NSFetchedResultsController<Event> {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest: NSFetchRequest<Event> = Event.fetchRequest()
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 20
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: "timestamp", ascending: false)
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
//--- Added to start Master-Detail project with Core Data
// Filtering results with Predicate
if let aSearchtemplate = searchtemplate {
let timeInterval = Double(aSearchtemplate)
if timeInterval != nil {
let time = Date().addingTimeInterval(-timeInterval!)
taskPredicate = NSPredicate(format: "Events.timestamp > %@", time as NSDate)
_fetchedResultsController?.fetchRequest.predicate = taskPredicate
}
}
//---//
do {
try _fetchedResultsController!.performFetch()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() 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.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
return _fetchedResultsController!
}
var _fetchedResultsController: NSFetchedResultsController<Event>? = nil
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
switch type {
case .insert:
tableView.insertSections(IndexSet(integer: sectionIndex), with: .fade)
case .delete:
tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade)
default:
return
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .fade)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .fade)
case .update:
configureCell(tableView.cellForRow(at: indexPath!)!, withEvent: anObject as! Event)
case .move:
configureCell(tableView.cellForRow(at: indexPath!)!, withEvent: anObject as! Event)
tableView.moveRow(at: indexPath!, to: newIndexPath!)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
/*
// Implementing the above methods to update the table view in response to individual changes may have performance implications if a large number of changes are made simultaneously. If this proves to be an issue, you can instead just implement controllerDidChangeContent: which notifies the delegate that all section and object changes have been processed.
func controllerDidChangeContent(controller: NSFetchedResultsController) {
// In the simplest, most efficient, case, reload the table view.
tableView.reloadData()
}
*/
}
//--- Added to start Master-Detail project with Core Data
extension MasterViewController: UISearchBarDelegate {
// MARK: - UISearchBar Delegate
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
filterContentForSearchText(searchBar.text!, scope: searchBar.scopeButtonTitles![selectedScope])
}
}
extension MasterViewController: UISearchResultsUpdating {
// MARK: - UISearchResultsUpdating Delegate
func updateSearchResults(for searchController: UISearchController) {
let searchBar = searchController.searchBar
let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex]
filterContentForSearchText(searchController.searchBar.text!, scope: scope)
}
}
//---//
Upvotes: 1
Views: 1399
Reputation: 1117
I found it!
TableView won't be refreshing if you leave the cache. So, before every new predicate is going to be applied you have to do this: NSFetchedResultsController.deleteCache(withName: nil)
Where 'nil' should be replaced to your cache file name if you use several.
And you have to address the class itself for this function. Not the instance.
So, thanks to Vadian, we can update only this part:
var fetchPredicate : NSPredicate? {
didSet {
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: nil)
fetchedResultsController.fetchRequest.predicate = fetchPredicate
}
}
Thank you, Vadian, thank you, Scott, for your inputs!
Upvotes: 0
Reputation: 285039
Don't put the logic to change the predicate in the declaration closure of fetchedResultsController
. Declare a predicate property
var fetchPredicate : NSPredicate? {
didSet {
fetchedResultsController.fetchRequest.predicate = fetchPredicate
}
}
and declare fetchedResultsController
lazily in the Swift way without the ugly objectivecish backing instance variable:
lazy var fetchedResultsController: NSFetchedResultsController<Event> = {
let fetchRequest: NSFetchRequest<Event> = Event.fetchRequest()
fetchRequest.predicate = self.fetchPredicate
fetchRequest.fetchBatchSize = 20
let sortDescriptor = NSSortDescriptor(key: "timestamp", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: nil, cacheName: "Master")
aFetchedResultsController.delegate = self
do {
try aFetchedResultsController.performFetch()
} catch let error as NSError{
fatalError("Unresolved error \(error), \(error.userInfo)")
}
return aFetchedResultsController
}()
Then set fetchPredicate
depending on search / not search, perform a fetch and reload the table.
Upvotes: 1
Reputation: 23701
You haven't done anything to ask the fetchedResultsController to fetch new data. The first thing you do in the getter is:
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
When reloadData()
is called, the framework calls tableView:cellForRowAtIndexPath:
which asks for the fetchedResultsController
, and just gets back the same fetch results you had previously.
When you change your search bar text you're going to have to do something to change the set of items returned by your tableview's data source.
Upvotes: 2