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?
}