Collectives™ on Stack Overflow

Find centralized, trusted content and collaborate around the technologies you use most.

Learn more about Collectives

Teams

Q&A for work

Connect and share knowledge within a single location that is structured and easy to search.

Learn more about Teams

I'm not finding an answer to the issue that I'm having. I'm using NSFetchedResultsControllerDelegete to connect my table view with my data. I'm retrieving the data with sectionNameKeyPath set so that I can group items in the table view. If I have only one item in a section of the table view and delete it, then I get the following error:

Invalid update: invalid number of sections. The number of sections contained in the table view after the update (1) must be equal to the number of sections contained in the table view before the update (2), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).

Here is my screen before the delete:

Here's my code for the view:

import CoreData
class VehicleTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
    var deleteItemIndexPath: IndexPath? = nil
    lazy var dao: DAOUtilities = {
        return DAOUtilities(context: GlobalVariables.getContext())
    lazy var fetchedResultsController: NSFetchedResultsController<Vehicles> = {
        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Vehicles")
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "activeFlag", ascending: false), NSSortDescriptor(key: "vehicleDesc", ascending: true)]
        // Initialize Fetched Results Controller
        let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: GlobalVariables.getContext(), sectionNameKeyPath: "activeFlag", cacheName: nil)
        // Configure Fetched Results Controller
        fetchedResultsController.delegate = self
        return fetchedResultsController as! NSFetchedResultsController<Vehicles>
    override func viewDidLoad() {
        super.viewDidLoad()
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
            try self.fetchedResultsController.performFetch()
        catch {
            let fetchError = error as NSError
            print("\(fetchError), \(fetchError.userInfo)")
        tableView.reloadData()
    // MARK: - Table view data source
    override func numberOfSections(in tableView: UITableView) -> Int {
        if let sections = fetchedResultsController.sections {
            return sections.count
        return 0
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if let sections = fetchedResultsController.sections {
            let sectionInfo = sections[section]
            return sectionInfo.numberOfObjects
        return 0
    override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let view = UIView()
        view.backgroundColor = UIColor(red: 55/255, green: 120/255, blue: 250/255, alpha: 1)
        guard let sectionInfo = fetchedResultsController.sections?[section] else {
            return view
        let title = UILabel()
        title.font = UIFont.boldSystemFont(ofSize: 16)
        title.textColor = .white
        title.text = sectionInfo.name == "1" ? "Active" : "Inactive"
        view.addSubview(title)
        title.translatesAutoresizingMaskIntoConstraints = false
        title.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        title.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16).isActive = true
        return view
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "vehicleCell", for: indexPath)
        if indexPath.row % 2 == 0 {
            cell.backgroundColor = UIColor.clear
        else {
            cell.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2)
        let vehicle = fetchedResultsController.object(at: indexPath)
        cell.textLabel!.text = vehicle.vehicleDesc
        return cell
    // Override to support conditional editing of the table view.
    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    // Override to support editing the table view.
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            deleteItemIndexPath = indexPath
            let vehicle = fetchedResultsController.object(at: indexPath)
            confirmDelete(itemToDelete: vehicle.vehicleDesc!)
    func confirmDelete(itemToDelete: String) {
        let alert = UIAlertController(title: "Delete Vehicle", message: "Are you sure you want to delete vehicle \(itemToDelete)", preferredStyle: .actionSheet)
        let deleteAction = UIAlertAction(title: "Delete", style: .destructive, handler: handleDeleteItem)
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: cancelDeleteItem)
        alert.addAction(deleteAction)
        alert.addAction(cancelAction)
        self.present(alert, animated: true, completion: nil)
    func handleDeleteItem(alertAction: UIAlertAction!) -> Void {
        if let indexPath = deleteItemIndexPath {
            let vehicle = fetchedResultsController.object(at: indexPath)
            let route = dao.getRouteForVehicle(vehicleId: vehicle.vehicleId!)
            if let _ = route {
                vehicle.activeFlag = false
            else {
                GlobalVariables.getContext().delete(vehicle)
    func cancelDeleteItem(alertAction: UIAlertAction!) {
        deleteItemIndexPath = nil
    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 .insert:
            if let indexPath = newIndexPath {
                tableView.insertRows(at: [indexPath], with: .fade)
            break;
        case .delete:
            if let indexPath = indexPath {
                tableView.deleteRows(at: [indexPath], with: .fade)
                tableView.reloadData()
            break;
//        case .update:
//            if let indexPath = indexPath {
//                let cell = tableView.cellForRow(at: indexPath)
//                configureCell(cell, atIndexPath: indexPath)
//            }
//            break;
        case .move:
            if let indexPath = indexPath {
                tableView.deleteRows(at: [indexPath], with: .fade)
            if let newIndexPath = newIndexPath {
                tableView.insertRows(at: [newIndexPath], with: .fade)
            break;
        default:
            break
    // MARK: - segue
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "editVehicleSegue" {
            if let vehicleEditViewController = segue.destination as? VehicleEditViewController {
                if let indexPath = tableView.indexPathForSelectedRow {
                    let vehicle = fetchedResultsController.object(at: indexPath)
                    vehicleEditViewController.vehicle = vehicle
                    vehicleEditViewController.tableView = tableView
//    // MARK: - Actions
//    @IBAction func btnAdd_ACTION(_ sender: UIBarButtonItem) {
//        var emptyFound = false
//        for i in 0..<vehicles.count {
//            let vehicle = vehicles[i]
//            if vehicle.isEmpty {
//                emptyFound = true
//                break
//            }
//        }
//        if !emptyFound {
//            vehicles.append("")
//            tableView.reloadData()
//        }
//    }

When you remove the last row from a section, you need to let the table view that the whole section has been removed.

You can do this by implementing

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, 
           didChange sectionInfo: NSFetchedResultsSectionInfo, 
      atSectionIndex sectionIndex: Int, 
                 for type: NSFetchedResultsChangeType) {
    let section = IndexSet(integer: sectionIndex)
    switch type {
        case .delete:
            tableView.deleteSections(section, with: .automatic)
        case .insert:
            tableView.insertSections(section, with: .automatic)
                Most tutorials and examples only use a single section, so they don't cover this function.
– Paulw11
                Nov 8, 2019 at 3:47
        

Thanks for contributing an answer to Stack Overflow!

  • Please be sure to answer the question. Provide details and share your research!

But avoid

  • Asking for help, clarification, or responding to other answers.
  • Making statements based on opinion; back them up with references or personal experience.

To learn more, see our tips on writing great answers.