I have a UITableView which populate it cells with a NSFetchedResultsController based on CoreData attribute isForConverter
which is Bool and should be true to be displayed. isForConverter
state sets in another ViewController.
When I want to delete some cells from the UITableView and after access cells which wasn't deleted I receive the error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'no object at index 5 in section at index 0'
There is a GIF with the problem: https://cln.sh/M1aI9Z
My code for deleting cells. I don't need to delete it from database, just change it isForConverter
from true to false:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let currency = fetchedResultsController.object(at: indexPath)
currency.isForConverter = false
coreDataManager.save()
}
}
NSFetchedResultsController Setup and delegates:
func setupFetchedResultsController() {
let predicate = NSPredicate(format: "isForConverter == YES")
fetchedResultsController = coreDataManager.createCurrencyFetchedResultsController(with: predicate)
fetchedResultsController.delegate = self
try? fetchedResultsController.performFetch()
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .update:
if let indexPath = indexPath {
tableView.reloadRows(at: [indexPath], with: .none)
}
case .move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
tableView.moveRow(at: indexPath, to: newIndexPath)
}
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .none)
}
case .insert:
if let newIndexPath = newIndexPath {
tableView.insertRows(at: [newIndexPath], with: .none)
}
default:
tableView.reloadData()
}
}
}
I noticed that if I just add tableView.reloadData()
to tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
then everything works good. But deletion animation is really fast and antsy. Also according to docs I should not use tableView.reloadData()
with NSFetchedResultsController...
How to fix that behaviour?
how can I define the indexPath for selected cell and avoid using tag for it in textFieldDidBeginEditing
What I would try:
- get the parent cell of the textField (take care, cell's have an extra contentView layer);
guard let cell = textField.superview?.superview as? ConverterTableViewCell else { return }
should provide it (if textField is not embedded in other view inside cell) ; otherwise, need to loop through superviews hierarchy of textField until you reach a UITableViewCell or more precisely ConverterTableViewCell)
- get its indexPath:
guard let indexPath = tableView.indexPath(for: cell) else { return }
Code becomes:
func textFieldDidBeginEditing(_ textField: UITextField) {
guard let cell = textField.superview?.superview as? ConverterTableViewCell else { return }
guard let indexPath = tableView.indexPath(for: cell) else { return }
pickedCurrency = fetchedResultsController.object(at: indexPath)
Hope that helps.