Search code examples
swiftcore-datansfetchedresultscontrolleruisearchcontroller

NSFetchedResultsController and Searching - Return multiple and single sections?


EDIT:

Shout out to pbasdf for helping me solve this issue.

Fixed code:

   lazy var fetchedResultsController = self.getFetchedResultsController()
    var sectionKeyPath: String? = #keyPath(Object.sectionKey)
    var searchPredicate: NSCompoundPredicate?
   
    // MARK: - Return FRC:
    private func getFetchedResultsController() -> NSFetchedResultsController<Object> { // var fetchedResultsController:
        print("Lazy: getFetchedResultsController()")
        let fetchRequest: NSFetchRequest<Object> = Object.fetchRequest()
        fetchRequest.predicate = searchPredicate
        
        let sortByKey = NSSortDescriptor(key: #keyPath(Object.sectionKey), ascending: true)
        let sortByName = NSSortDescriptor(key: #keyPath(Object.name), ascending: true)
        
        fetchRequest.sortDescriptors = [sortByKey, sortByName]
        fetchRequest.fetchBatchSize = 20
        
        let fetchedResultsController = NSFetchedResultsController(
            fetchRequest: fetchRequest,
            managedObjectContext: coreDataStack.managedContext,
            sectionNameKeyPath: sectionKeyPath ?? nil,
            cacheName: nil)
        
        fetchedResultsController.delegate = self
        return fetchedResultsController
    }
    
    private func refreshFRC() {
        fetchedResultsController = getFetchedResultsController() // Reset FRC
        do {  // Load Data:
            try fetchedResultsController.performFetch()
        } catch let error as NSError {
            print("Fetching error: \(error), \(error.userInfo)")
        }
    }

This gives you the FRC with an optional predicate and sectionNameKeyPath. Which you can then set to your needs, and then set the changes with refreshFRC().


I'm working on adding search to a tableview using a NSFetchedResultsController. My goal:

  • Return multiple sections based on first letter of object
  • Return all objects into a single section when searching.

I have working code. And I can make the table do both depending on my sectionKey, I just cant figure out how to do both in the same build.

Is this normal behavior and I'm trying to do something thats not possible by changing the FRC's sectionNameKeyPath and sortDescriptors? Or am I just missing something?

private func getFetchedResultsController() -> NSFetchedResultsController<Object> {
        let fetchRequest: NSFetchRequest<Object> = Object.fetchRequest()
        
        let sortByKey = NSSortDescriptor(key: #keyPath(Object.sectionKey), ascending: true)
        let sortByName = NSSortDescriptor(key: #keyPath(Object.name), ascending: true)
        
        switch sectionKeyPath {
        case nil:
            fetchRequest.sortDescriptors = nil
            fetchRequest.fetchBatchSize = 20
        default:
            fetchRequest.sortDescriptors = [sortByKey, sortByName]
            fetchRequest.fetchBatchSize = 20
        }
        fetchRequest.sortDescriptors = [sortByKey, sortByName]
        fetchRequest.fetchBatchSize = 20
        
        let fetchedResultsController = NSFetchedResultsController(
            fetchRequest: fetchRequest,
            managedObjectContext: coreDataStack.managedContext,
            sectionNameKeyPath: sectionKeyPath ?? nil,
            cacheName: nil)
        fetchedResultsController.delegate = self
        return fetchedResultsController
    }

I'm also curious if it's better to use a a single FRC for the entire viewController, or if it would be a better approach to make one for the entire list of objects, and a second only for when the search is active?


func updateSearchResults(for searchController: UISearchController) {
    let searchBar = searchController.searchBar
    searchBar.barStyle = .default
    
    switch searchBar.text?.count {
    case nil:
        searchPredicate = nil
        sectionKeyPath = #keyPath(Object.sectionKey)
        tableView.reloadData()
    case 0:
        searchPredicate = nil
        sectionKeyPath = #keyPath(Object.sectionKey)
        tableView.reloadData()
    default:
        sectionKeyPath = nil
        guard let searchText = searchBar.text else { return }
        setSearchPredicate(search: searchText)
    }
    fetchFRC()
    tableView.reloadData()
} // End: updateSearchResults()

func fetchFRC() {
    do {
        try fetchedResultsController.performFetch()
    } catch let error as NSError {
        print("Fetching error: \(error), \(error.userInfo)")
    }
}

Solution

  • As per comments:

    1. You need to re-call getFetchedResultsController before fetchFRC in that updateSearchResults code; and
    2. You need to assign the result to the fetchedResultsController var defined in your view controller.