Search code examples
swiftuitableviewcore-datarowsnsexception

NSInternalInconsistencyException: Invalid Update using tableview CoreData


I am using a tableView to display a list of people. I am trying to add an alert to confirm that the user actually wants to delete the person and to prevent mistakes. However, when I try to delete the person that is stored with CoreData, there seems to be a problem reloading the view. I get this exception: Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '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 (2), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'

Editing and Delete Function:

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == .Delete {

        // Delete the row from the data source
        var deleteRow = indexPath.row

        indexPathforDelete = indexPath

        let entityDescription = NSEntityDescription.entityForName("People", inManagedObjectContext: managedObjectContext!)
        let request = NSFetchRequest()
        request.entity = entityDescription

        var error: NSError?

        var objects = managedObjectContext?.executeFetchRequest(request, error: &error)

        if let results = objects {

            let personToDelete = results[deleteRow] as! NSManagedObject
            let firstName = personToDelete.valueForKey("firstName") as! String
            let lastName = personToDelete.valueForKey("lastName") as! String

            var message = "Are you sure you would like to delete \(firstName) \(lastName)?\nThis will permanentaly remove all records of "

            if(personToDelete.valueForKey("gender") as! String == "Male"){

                message = "\(message)him."

            }

            else{

                println(personToDelete.valueForKey("gender") as! String)

                message = "\(message)her."

            }

            var deleteAlert : UIAlertView = UIAlertView(title: "Delete \(firstName) \(lastName)", message: message, delegate: self, cancelButtonTitle: "Cancel")

            deleteAlert.addButtonWithTitle("Delete")

            deleteAlert.show()

        }

        save()

    } else if editingStyle == .Insert {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view

    }    
}

AlertView Response Function:

func alertView(alertView: UIAlertView, clickedButtonAtIndex buttonIndex: Int){

    if(buttonIndex == 1){
        managedObjectContext?.deleteObject(personToDelete)
        tableView.deleteRowsAtIndexPaths([indexPathforDelete], withRowAnimation: .Fade)
        save()
    }

    setEditing(false, animated: true)
    self.navigationItem.leftBarButtonItem = nil

}

tableView number of rows function:

var personToDelete = NSManagedObject()
var indexPathforDelete = NSIndexPath()

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // #warning Incomplete method implementation.
    // Return the number of rows in the section.

    let entityDescription = NSEntityDescription.entityForName("People", inManagedObjectContext: managedObjectContext!)
    let request = NSFetchRequest()
    request.entity = entityDescription

    var error: NSError?

    var objects = managedObjectContext?.executeFetchRequest(request, error: &error)

    let results = objects

    println("Results Count: \(results!.count)")

    return results!.count

}

Solution

  • I think the problem is that you have two variables with the name propertyToDelete: a property that you declare and initialise with a blank NSManagedObject:

            var personToDelete = NSManagedObject()
    

    and a local variable that you declare within your commitEditingStyle function:

            let personToDelete = results[deleteRow] as! NSManagedObject
    

    It is this local variable to which you assign the object from your results array. But this local variable is destroyed when the function completes, and the AlertView action is deleting the object to which the property points. (The reason I hesitate is that I would expect your context to throw an error when it tries to delete an object that has never been registered with it). Note that by contrast you have only the one variable named indexPathforDelete. This holds the correct value when the AlertView action runs, and consequently the tableView deletes the correct row. That's why you get the error: it has deleted a row, but then finds (because no object has been deleted) it still has the same number of rows as before.

    The immediate solution is to use the property within your function, rather than a local variable: just delete let:

            personToDelete = results[deleteRow] as! NSManagedObject
    

    But I would also recommend rethinking your approach: you are repeating the same fetch. If all the datasource methods do the same, it will be repeated numerous times when the table view is first built, whenever a cell is scrolled into view, whenever a cell is tapped, etc. This will be costly in terms of performance. You should instead undertake the fetch once (perhaps in viewDidLoad), store the results in an array property, and use that for the table view datasource methods. Alternatively, and perhaps preferably, use an NSFetchedResultsController: it is very efficient and there is boilerplate code for updating the table view when objects are added or deleted.