Search code examples
swiftuitableviewuikitnsfetchedresultscontroller

NSFetchedResultsController error: 'no object at index in section at index 0'


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?

UPDATE:

It seems I found out what the reason of that crash was. This is what my print() tryings gave: SCREENSHOT. What is a pickedCurrency: this is a global variable of custom type Currency which I created to receive its attribute currentValue (Double, 87.88). I need that value only from the picked to edit cell. After I use that value for calculation at cellForRowAt() and result of the calculation fills all other cells which is not in the edit mode now.

I define pickedCurrency in textFieldDidBeginEditing() because there I receive the exact row of Currency I picked to edit:

func textFieldDidBeginEditing(_ textField: UITextField) {
    pickedCurrency = fetchedResultsController.object(at: IndexPath(row: textField.tag, section: 0))
    numberFromTextField = 0
    textField.textColor = UIColor(named: "BlueColor")
    textField.placeholder = "0"
    textField.text = ""
}

And then use it's value in cellForRowAt to calculate all other cells values based on pickedCell value and number I put in a textField of activeCell:

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "converterCell", for: indexPath) as! ConverterTableViewCell
    
    let currency = fetchedResultsController.object(at: indexPath)
    cell.flag.image = currencyManager.showCurrencyFlag(currency.shortName ?? "notFound")
    cell.shortName.text = currency.shortName
    cell.fullName.text = currency.fullName
    cell.numberTextField.tag = indexPath.row
    cell.numberTextField.delegate = self
    
    if let number = numberFromTextField, let pickedCurrency = pickedCurrency {
        cell.numberTextField.text = currencyManager.performCalculation(with: number, pickedCurrency, currency)
    }
    return cell
}

It seems when I delete a lot of cells and then click on random cell to edit it's not updates its IndexPath(row: textField.tag, section: 0)...

Maybe there is a way to receive Currency object I picked for editing in cellForRowAt()?


Solution

  • I am not certain if this will work but it seems too long for it to be a comment so give the below a try.

    Initially I told you could use tag to identify a specific view which is good for quick and simple implementations but when rows get moved / deleted as we have it now, it will be very difficult to manage using tags as you have to constantly update them.

    For static table views, they are fine but if your rows will change, then more sophisticated patterns like delegate / observer might be better.

    Anyways, what I think can help your situation for now in textFieldDidBeginEditing is stop using the tag to get the index path and get the indexpath from what is tapped.

    I still think maybe delegate pattern is better but this might work for you:

    func textFieldDidBeginEditing(_ textField: UITextField) {
        // Get the coordinates of where we tapped in the table
        let tapLocation = textField.convert(textField.bounds.origin,
                                            to: tableView)
        
        if let indexPath = self.tableView.indexPathForRow(at: tapLocation)
        {
            // don't use tag of textfield anymore
            pickedCurrency
                = fetchedResultsController.object(at: IndexPath(row: indexPath,
                                                                section: 0))
            
            numberFromTextField = 0
            textField.textColor = UIColor(named: "BlueColor")
            textField.placeholder = "0"
            textField.text = ""
        }
    }
    

    Does this help ?