简体   繁体   中英

Assertion failure UITableView: delete object which a relationship to another object

I try to delete a present which is assosiated to a person. Now when run the programm and try to delete one present object an error occurs:

HappyBirthdayMom[7907:369895] * Assertion failure in -[UITableView _Bug_Detected_In_Client_Of_UITableView_Invalid_Number_Of_Rows_In_Section:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3900.12.16/UITableView.m:2406 2019-12-13 14:34:55.941744+0100 HappyBirthdayMom[7907:369895] * 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).'

I don't really get how to solve this problem - the count of the giftArray should return the number of rows.


import UIKit
import CoreData

class PresentViewController: UITableViewController {

    @IBOutlet weak var deleteButton: UIBarButtonItem!

    var managedContext : NSManagedObjectContext?

    var person: Person?

    var giftArray: [Geschenk] {
        return person?.geschenke?.allObjects as? [Geschenk] ?? [Geschenk]()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }


    // MARK: - Table View

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return giftArray.count

    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "GeschenkCell", for: indexPath)

        let present = giftArray[indexPath.row]

        cell.textLabel?.text = present.title

        //Tenary operator ==>
        // value = condidtion ? valueIfTrue : valueIfFalse
        cell.accessoryType = present.isOwned ? .checkmark : .none

        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        //print(itemArray[indexPath.row])


        giftArray[indexPath.row].isOwned = !giftArray[indexPath.row].isOwned

        saveGift()

        tableView.deselectRow(at: indexPath, animated: true)
    }

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            managedContext!.delete(giftArray[indexPath.row] as NSManagedObject)
            tableView.deleteRows(at: [indexPath], with: .fade)

            do {
                try managedContext!.save()
            } catch {
                ("Error delete gift \(error)")
            }
            self.reloadPerson()
        }
    }

    // MARK: - Actions

    @IBAction func addButtonPressed(_ sender: UIBarButtonItem) {
        var textField = UITextField()

        let alert = UIAlertController(title: "Neues Geschenk hinzufügen", message: "", preferredStyle: .alert)

        let action = UIAlertAction(title: "Hinzufügen", style: .default) { (action) in

            let newGift = Geschenk(context: self.managedContext!)
            newGift.title = textField.text!
            newGift.isOwned = false
            newGift.owner = self.person

            self.saveGift()
        }

        let actionCancel = UIAlertAction(title: "Abbrechen", style: .cancel) {(action) -> Void in

        }

        alert.addTextField{ (alertTextfield) in
            alertTextfield.placeholder = "Neues Geschenk hinzufügen"
            textField = alertTextfield
        }

        alert.addAction(action)
        alert.addAction(actionCancel)

        present(alert, animated: true, completion: nil)
    }

    @IBAction func editButtonPressed(_ sender: UIBarButtonItem) {

        self.tableView.isEditing = !self.tableView.isEditing
        sender.title = (self.tableView.isEditing) ? "Erledigt" : "Löschen"

    }


    @IBAction func backButtonPressed(_ sender: UIBarButtonItem) {

        self.dismiss(animated: true, completion: nil)
    }


    // MARK: - Data Manipulation

    func saveGift() {
        do {
            try managedContext?.save()

        }catch {
            ("Error saving Gift \(error)")
        }

        self.reloadPerson()
        self.tableView.reloadData()
    }

    func reloadPerson() {
        self.person = self.managedContext?.object(with: self.person!.objectID) as? Person
    }
}

I believe it's because delete row action is taking place before your managedContext has had a chance to update it's count. Therefore, table row count is 1 count less than managedContext.

Try this:

override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
        managedContext!.delete(giftArray[indexPath.row] as NSManagedObject)

        do {
            try managedContext!.save()
            tableView.reloadData() //placing this after managedContext saves and reflects new count.
        } catch {
            ("Error delete gift \(error)")
        }
        self.reloadPerson()
    }
}

The problem is that the datasource array and the table view is not in sync.

My suggestion is to set the data source array once in viewDidLoad and handle all changes explicitly. The computed variable is called very often and is unneccessarily expensive.

The benefit is you get the nice animations when cells are inserted or removed.

Replace the data source array and viewDidLoad with

var giftArray = [Geschenk]()

override func viewDidLoad() {
    super.viewDidLoad()
    giftArray = person?.geschenke?.allObjects as? [Geschenk] ?? []
    tableView.reloadData()
}

Replace tableView(commit: with

override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
        let objectToDelete = giftArray.remove(at: indexPath.row)
        managedContext!.delete(objectToDelete)
        tableView.deleteRows(at: [indexPath], with: .fade)
        saveGift()
    }
}

Replace the "Hinzufügen" action with

let action = UIAlertAction(title: "Hinzufügen", style: .default) { [weak self] (action) in

        let newGift = Geschenk(context: self.managedContext!)
        newGift.title = textField.text!
        newGift.isOwned = false
        newGift.owner = self.person
        let insertionIndex = self?.giftArray.count
        self?.giftArray.append(newGift)
        self?.tableView.insertRows(at: IndexPath(row: insertionIndex, section: 0), with: .automatic)
        self?.saveGift()
 }

Replace saveGift with

func saveGift() {
    do {
        try managedContext?.save()

    } catch {
        ("Error saving Gift \(error)")
    }
}

Delete

 
 
  
   func reloadPerson() { self.person = self.managedContext?.object(with: self.person!.objectID) as? Person }
 
 

The objects are properly saved to the Core Data context but indepenently maintained in the controller.

A powerful alternative – which can drive the entire UI pretty smartly – is NSFetchedResultsController with appropriate predicate.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM