Alexey Chekanov
Alexey Chekanov

Reputation: 1117

Search Core Data in Swift

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

Answers (3)

Alexey Chekanov
Alexey Chekanov

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

vadian
vadian

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 fetchedResultsControllerlazily 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

Scott Thompson
Scott Thompson

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

Related Questions