1

I'm using Swift 3 in Xcode 8 beta 6, targeting iOS 10.0. I am implementing a simple UISearchController in a UITableView backed with an NSFetchedResultsController. I have two properties

var patients = [Patient]() // Assigned to fetchedResultsController.fetchedObjects when the fetch is performed, and when the moc is updated.
var searchResults = [Patient]()

In my updateSearchResults(for searchController: UISearchController) method, I do this:

func updateSearchResults(for searchController: UISearchController) {
    if let searchText = searchController.searchBar.text {
        self.searchResults = people.filter {
        return $0.lastName!.localizedCaseInsensitiveContains(searchText)
    }

Using breakpoints, I've identified that the code gets as far as the filter method, but doesn't enter it, failing with:

fatal error: NSArray element failed to match the Swift Array Element type

I've looked at a bunch of the other SO questions involving this error, but none have helped. I've also tried explicitly casting people in the updateSearchResults method, but no luck. Thoughts?

UPDATE Complete code for tableViewController and Patient subclass:

import UIKit
import CoreData

class PatientsListViewController: UITableViewController, NSFetchedResultsControllerDelegate, UISearchResultsUpdating {

    enum SegueIdentifier: String {
        case showPatientDetail
    }
    //MARK: Properties
    var managedObjectContext: NSManagedObjectContext!
    var fetchedResultController: NSFetchedResultsController<Patient>!
    var searchController: UISearchController!

    var searchResults: [Patient] = []
    var patients: [Patient] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        let fetchRequest: NSFetchRequest<Patient> = Patient.fetchRequest()
        let sortDescriptor = NSSortDescriptor(key: "lastName", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor]

        fetchedResultController = NSFetchedResultsController<Patient>(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
        fetchedResultController.delegate = self

        do{
            try fetchedResultController.performFetch()
            patients = fetchedResultController.fetchedObjects!
        }catch{
            print(error)
        }

        //Add Search bar to the table header
        searchController = UISearchController(searchResultsController: nil)
        tableView.tableHeaderView = searchController.searchBar
        searchController.searchResultsUpdater = self
        searchController.dimsBackgroundDuringPresentation = false
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        guard let numberOfSections = fetchedResultController.sections?.count else {
            return 0
        }
        return numberOfSections
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        //        let section = fetchedResultController.sections![section]
        //        let numberOfRows = section.numberOfObjects
        if searchController.isActive {
            return searchResults.count
        } else {
            return fetchedResultController.sections![section].numberOfObjects
        }

    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: PatientCell.reuseIdentifier, for: indexPath) as! PatientCell
        let patient = (searchController.isActive) ? searchResults[indexPath.row] : fetchedResultController.object(at: indexPath)
        cell.configure(with: patient)

        return cell
    }

    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        if searchController.isActive{
            return false
        }else{
            return true
        }
    }

    override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
        let deleteAction = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) -> Void in
            let patientToDelete = self.fetchedResultController.object(at: indexPath)
            self.managedObjectContext.delete(patientToDelete)

            do{
                try self.managedObjectContext.save()
            }catch{
                print(error)
            }

        }
        return [deleteAction]
    }



    // MARK: - FetchedResultsController delegate

    // Notify the tableView that updates will begin
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.beginUpdates()
    }

    //Cover all cases of row changes like move, delete, insert, update
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch  type{
        case .insert:
            if let newIndexPath = newIndexPath{
                tableView.insertRows(at: [newIndexPath], with: .fade)
            }
        case .delete:
            if let indexPath = indexPath{
                tableView.deleteRows(at: [indexPath], with: .fade)
            }
        case .update:
            if let indexPath = indexPath{
                tableView.reloadRows(at: [indexPath], with: .fade)
            }
        case .move:
            break
        }

        patients = controller.fetchedObjects as! [Patient]
    }

    // Notify the tableView that updates are done
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.endUpdates()
    }

    //Pass Patient to PatientDetailViewController
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        guard let identifier = segue.identifier.flatMap(SegueIdentifier.init) else { return }

        switch identifier {
        case .showPatientDetail:
            guard let indexPath = tableView.indexPathForSelectedRow else {
                fatalError("No row selected in tableView")
            }
            let destinationController = segue.destination as! PatientDetailViewController
            destinationController.patient = (searchController.isActive) ? searchResults[indexPath.row] : fetchedResultController.object(at: indexPath)
        }
    }

    //Implement Search Bar

    func filterContent(for searchText:String) {
        searchResults = patients.filter( { patient -> Bool in
            let nameMatch = patient.lastName?.localizedCaseInsensitiveContains(searchText)
            return nameMatch != nil
        })
    }

    func updateSearchResults(for searchController: UISearchController) {
        if let searchText = searchController.searchBar.text {
            filterContent(for: searchText)
            tableView.reloadData()
        }
    }

}

PATIENT:

@objc(Patient)
public class Patient: NSManagedObject {


}


extension Patient {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Patient> {
        return NSFetchRequest<Patient>(entityName: "Patient");
    }

    @NSManaged public var address: String?
    @NSManaged public var dateOfBirth: String?
    @NSManaged public var firstName: String?
    @NSManaged public var gender: String?
    @NSManaged public var lastName: String?

}
jjatie
  • 5,152
  • 5
  • 34
  • 56
  • This happens even when you comment out the fetched results controller, as shown in your code? Are you showing the _actual code that crashes_? – matt Aug 26 '16 at 20:59
  • Also it's quite important to know exactly what version of Swift / Xcode this is. Things have changed for `fetchedObjects` in a rather dramatic way recently, because NSFetchedResultsController is now a generic. If you didn't creates this thing as a `NSFetchedResultsController` you are naturally going to be in trouble later... So could we see the code that does the fetching and the assigning? And get some version info on all this? – matt Aug 26 '16 at 21:02
  • This is the exact code I'm using. I'm using Swift 3 in Xcode 8 beta 6, targeting iOS 10.0 – jjatie Aug 26 '16 at 21:05
  • But it's not enough of the code to reproduce. If `people = [Person]()` and `searchResults = [Person]()` I'm not going to have any trouble filtering `people` and assigning the result to `searchResults`. Do you see? I want to crash just like you. Make me a minimal complete verifiable example... – matt Aug 26 '16 at 21:42
  • Check what do `fetchedObjects` contains when assigning it to the `people` property. Most likely you have objects that are not instances of `Person` there, causing the crash. – Cristik Aug 27 '16 at 03:41
  • @Cristik I tried explicitly casting `fetchedObjects as Array`, but it had no effect. – jjatie Aug 27 '16 at 13:41
  • What do you get if you put `print(type(of: patients[0]))` at the top of `filterContent(for:)`? – OOPer Aug 27 '16 at 14:28
  • `type(of: patients[0])` crashes with the same error, but `type(of: patients)` returns `Array` – jjatie Aug 27 '16 at 15:01
  • @jjatie casting doesn't help, as the cast operation doesn't change the actual contents of the array. Try logging the class names of the array elements in order to find out if you really have only `Person` instances there, e.g. `print(fetchedResultsController.fetchedObjects.valueForKeyPath("className"))` – Cristik Aug 28 '16 at 21:12
  • @Cristik fetchedObjects does not have the value(forKeyPath) method. – jjatie Aug 29 '16 at 21:48
  • Sorry, meant `valueForKey`, not `valueForKeyPath` – Cristik Aug 30 '16 at 03:35
  • Oops! So did I. value(forKey:) doesn't exist on fetchedObjects (or anything beginning with "value") – jjatie Aug 30 '16 at 10:08
  • Removing the `@objc(Patient)` above the class definition of `Patient` solved everything. I'm not sure the ramifications of this, but haven't encountered any Core Data issues yet – jjatie Aug 30 '16 at 14:23

0 Answers0