Invalid update: invalid number of rows in section 0 with NSFetchedResultsController

I have 2 ViewControllers:

VC1 populates its tableView based on CoreData attribute isPicked, which is bool and show only items with true state. VC2 is a second Modal (not Full Screen) View Controller which allow user to change the state of isPicked attribute: check and uncheck item (make it true or false). The idea is user picks needed items end dismiss the VC2 and the items should appear in VC1.

I have implemented NSFetchedResultsController to both VC1 and VC2. And as soon as I press on first item (i.e. change isPicked state to true) I receive the error from VC1:

[error] fault: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).

Here is how I change a state of item in VC2 (true\false):

    guard let currencyArray = fetchedResultsController.fetchedObjects else { return }
    let cell = tableView.cellForRow(at: indexPath) as! PickCurrencyTableViewCell
    
    for currency in currencyArray {
        if currency.shortName == cell.shortName.text {
           currency.isPicked = !currency.isPicked
            coreDataManager.save()
        }
    }
}

Here is my VC1 NSFetchedResultsController implementation:

    super.viewDidLoad()
    setupFetchedResultsController()
}

func setupFetchedResultsController() {
    let predicate = NSPredicate(format: "isForConverter == YES")
    fetchedResultsController = createCurrencyFetchedResultsController(and: predicate)
    fetchedResultsController.delegate = self
    try? fetchedResultsController.performFetch()
}

func createCurrencyFetchedResultsController(with request: NSFetchRequest<Currency> = Currency.fetchRequest(), and predicate: NSPredicate? = nil, sortDescriptor: [NSSortDescriptor] = [NSSortDescriptor(key: "shortName", ascending: true)]) -> NSFetchedResultsController<Currency> {
    request.predicate = predicate
    request.sortDescriptors = sortDescriptor
    
    return NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
}

Here is my VC1 NSFetchedResultsController delegate:

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?) {
    if let indexPath = indexPath, let newIndexPath = newIndexPath {
        switch type {
        case .update:
            tableView.reloadRows(at: [indexPath], with: .none)
        case .move:
            tableView.moveRow(at: indexPath, to: newIndexPath)
        case .delete:
            tableView.deleteRows(at: [indexPath], with: .none)
        case .insert:
            tableView.insertRows(at: [indexPath], with: .none)
        default:
            tableView.reloadData()
        }
    }
}
}

And finally my VC1 Table View methods:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let currency = fetchedResultsController.sections![section]
        return currency.numberOfObjects
    }
    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.shortName.text = currency.shortName
        cell.fullName.text = currency.fullName
        
        return cell
    }

When I reload the app, picked item shows itself in VC1 (which caused crash). But every change in VC2 crashes the app again with the same error. I don't have any methods to delete items in VC1 or so. I need that VC1 tableView just show or hide items according to isPicked state made from VC2.

What I missed in my code?

Answered by Claude31 in 702685022

That's normal for insert:

Doc states:

controller(_:didChange:at:for:newIndexPath:)

indexPath

The index path of the changed object (this value is nil for insertions).

So, you should change the code like this:

    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 {
                  tableView.moveRow(at: indexPath, to: newIndexPath)
 	           }
           case .delete:
                if let indexPath = indexPath {
                   tableView.deleteRows(at: [indexPath], with: .none)
                 tableView.reloadData()	// << May be needed
	           }
           case .insert:
                if let newIndexPath = newIndexPath {
                   tableView.insertRows(at: [newIndexPath], with: .none)
                   tableView.reloadData()	// << May be needed
 	           }
           default:
                tableView.reloadData()
            }
    }
}

I did not dig into your code, but the cause is in numberOfRowsInSection in VC1:

Probably, you delete / add row in VC1 tableView before or without datasource being updated.

Could you add a print to check what the number is ?

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let currency = fetchedResultsController.sections![section]
        print(currency.numberOfObjects)
        return currency.numberOfObjects
    }

What is the log ? Is it this print: print(currency.numberOfObjects)

So, why 13 or 14 values printed ?

Which func is called when you click a cell in VC2 ? Is it

        case .insert:
            tableView.insertRows(at: [indexPath], with: .none)

If so, you insert a row, but it seems you don't update the dataSource before .

I notice an important point:

  • At start, there are 13 values
  • when you select a country, then you add one (14 values)

Is it what you expect ?

What is the log ? Is it this print: print(currency.numberOfObjects)

Attach the log:

So, why 13 or 14 values printed ?

Can't understand too. At the start the table is empty... Why there is always 13 numbers and not 1,5,6 etc...

Which func is called when you click a cell in VC2 ?

It's

    let cell = tableView.cellForRow(at: indexPath) as! PickCurrencyTableViewCell
    
    for currency in currencyArray {
        if currency.shortName == cell.shortName.text {
           currency.isPicked = !currency.isPicked
            coreDataManager.save()
        }
    }
}

There on VC2 it sorts the array of items and if shortName (EUR) equals to the shortName of pickedCell then it change it isPicked CoreData attribute from false to true. And if I was able to click again then from true to false. And save it to database after. The NSFetchedResultController is setup to filter and show in VC1 tableView only the state isPicked = true.

Is it what you expect ?

Not quite sure, because I expect behaviour like I pressed on cell in VC2 and VC1 should load that currency I picked based on isPicked state...

My question was : what is the print statement that is logged ? Is it print(currency.numberOfObjects)?

Could you show the complete code for VC1 and VC2 ? We cannot really understand with small pieces of code.

Database already filled with data. I receive 33 objects and they all successfully loads with NSFetchedResultsController in VC2

VC1:

VC2:

Could you tell which case is triggered when you tap on a row in

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        if let indexPath = indexPath, let newIndexPath = newIndexPath {
            switch type {
            case .update:
                tableView.reloadRows(at: [indexPath], with: .none)
            case .move:
                tableView.moveRow(at: indexPath, to: newIndexPath)
            case .delete:
                tableView.deleteRows(at: [indexPath], with: .none)
            case .insert:
                tableView.insertRows(at: [indexPath], with: .none)
            default:
                tableView.reloadData()
            }
        }

When you deleteRow, you should reload data:

        if let indexPath = indexPath, let newIndexPath = newIndexPath {
            switch type {
            case .update:
                tableView.reloadRows(at: [indexPath], with: .none)
            case .move:
                tableView.moveRow(at: indexPath, to: newIndexPath)
            case .delete:
                tableView.deleteRows(at: [indexPath], with: .none)
                tableView.reloadData()
            case .insert:
                tableView.insertRows(at: [indexPath], with: .none)
                tableView.reloadData()
            default:
                tableView.reloadData()
            }
        }

Can you advise how can I do it? 

Just add some print in each case statement:

            switch type {
            case .update:
                print("update")
                tableView.reloadRows(at: [indexPath], with: .none)

So add a print before the switch, even before the if let, to see if method is called. And whether if let are successful. Why do you need to test newIndexPath for nil, except for .move case ?

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {

        print(#function, indexPath, newIndexPath)
        if let indexPath = indexPath, let newIndexPath = newIndexPath {

Which is nil ? indexPath or newIndexPath ? You have the reason of the problem now.

If you have written

print(#function, indexPath, newIndexPath)

and get

controller(_:didChange:at:for:newIndexPath:) nil Optional([0, 0])

That means that indexPath is nil.

Accepted Answer

That's normal for insert:

Doc states:

controller(_:didChange:at:for:newIndexPath:)

indexPath

The index path of the changed object (this value is nil for insertions).

So, you should change the code like this:

    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 {
                  tableView.moveRow(at: indexPath, to: newIndexPath)
 	           }
           case .delete:
                if let indexPath = indexPath {
                   tableView.deleteRows(at: [indexPath], with: .none)
                 tableView.reloadData()	// << May be needed
	           }
           case .insert:
                if let newIndexPath = newIndexPath {
                   tableView.insertRows(at: [newIndexPath], with: .none)
                   tableView.reloadData()	// << May be needed
 	           }
           default:
                tableView.reloadData()
            }
    }
}
Invalid update: invalid number of rows in section 0 with NSFetchedResultsController
 
 
Q