Search code examples
iosswiftcellnsfetchedresultscontroller

NSFetchedResultsController with custom Cell in Swift


I'd like to have a list of presenters (speakers) in a tableView. I'm using the "edit" button in the upper right corner of my tableView to unhide or hide a addSpeakerCell at the end of all objects that are currently being displayed. This works nicely and without problems. I can delete objects and toggle the edit button and everything works as it is supposed to.

But when getting to the addSpeakerView (via the addSpeakerCell) and add a speaker, when hitting the save-button and getting back to the tableView, the following warning appears:

Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (0), 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). with userInfo (null)

My guess is the addSpeakerCell is not updated with the new indexPath. But then I still couldn't find out how to solve it...

If someone could kindly point me to the right solution or even provide me with the necessary code-snippet, I would be eternally grateful

My code of the tableView:

SpeakersViewController:

class SpeakersViewController: UITableViewController
    {
        var managedObjectContext: NSManagedObjectContext!

        lazy var fetchedResultsController: NSFetchedResultsController =
        {
            let fetchRequest = NSFetchRequest()
            let entity = NSEntityDescription.entityForName("Speaker", inManagedObjectContext: self.managedObjectContext)
            fetchRequest.entity = entity
            let sortDescriptor = NSSortDescriptor(key: "firstName", ascending: true)
            fetchRequest.sortDescriptors = [sortDescriptor]
            fetchRequest.fetchBatchSize = 20
            let fetchedResultsController = NSFetchedResultsController(
                fetchRequest: fetchRequest,
                managedObjectContext: self.managedObjectContext,
                sectionNameKeyPath: nil,
                cacheName: "Speaker")
            fetchedResultsController.delegate = self
            return fetchedResultsController
        }()

        var singleEdit = false // indicates user is swipe-deleting a particular speaker

        override func viewDidLoad()
        {
            super.viewDidLoad()
            self.navigationItem.rightBarButtonItem = self.editButtonItem()
            tableView.allowsSelectionDuringEditing = true

            // CoreData Stuff
            NSFetchedResultsController.deleteCacheWithName("Speaker")
            performFetch()
        }

        func performFetch()
        {
            var error: NSError?
            if !fetchedResultsController.performFetch(&error)
            {
                print("An error occurred: \(error?.localizedDescription)")
            }
        }

        deinit
        {
            fetchedResultsController.delegate = nil
        }

        override func didReceiveMemoryWarning()
        {
            super.didReceiveMemoryWarning()
        }

        // Unhide or hide the AddSpeakerCell
        override func setEditing(editing: Bool, animated: Bool)
        {
            super.setEditing(editing, animated: true)

            if !singleEdit // if user is not swipe-deleting Speaker
            {
                self.navigationItem.setHidesBackButton(editing, animated: true)
            }

            let sectionInfo = fetchedResultsController.sections![0] as NSFetchedResultsSectionInfo
            let rows = sectionInfo.numberOfObjects
            let indexPath = NSIndexPath(forRow: rows, inSection: 0)
            let indexPaths = [indexPath]
            if (editing)
            {
                tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Top)
            }
            else
            {
                tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Top)
            }
        }

        // MARK: - Table view data source
        override func tableView(tableView: UITableView, editingStyleForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCellEditingStyle
        {
            let sectionInfo = fetchedResultsController.sections![0] as NSFetchedResultsSectionInfo
            if indexPath.row == sectionInfo.numberOfObjects
            {
                return .Insert
            }
            else
            {
                return .Delete
            }
        }

        // Unhide or hide the AddSpeakerCell
        override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
        {
            let sectionInfo = fetchedResultsController.sections![0] as NSFetchedResultsSectionInfo
            var rows = sectionInfo.numberOfObjects
            if (editing)
            {
                rows++
            }
            return rows
        }

        override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
        {
            let sectionInfo = fetchedResultsController.sections![0] as NSFetchedResultsSectionInfo

            if indexPath.row < sectionInfo.numberOfObjects
            {
                let cell = tableView.dequeueReusableCellWithIdentifier("SpeakerCell") as SpeakerCell
                let speaker = fetchedResultsController.objectAtIndexPath(indexPath) as Speaker
                cell.configureForSpeaker(speaker)
                return cell
            }
            else
            {
                let cell = tableView.dequeueReusableCellWithIdentifier("AddSpeakerCell") as UITableViewCell
                return cell
            }
        }

        override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
        {
            tableView.deselectRowAtIndexPath(indexPath, animated: true)
        }

        override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath)
        {
            if editingStyle == .Delete
            {
                let speaker = fetchedResultsController.objectAtIndexPath(indexPath) as Speaker
                managedObjectContext.deleteObject(speaker)

                var error: NSError?
                if !managedObjectContext.save(&error)
                {
                    print("An error occurred: \(error?.localizedDescription)")
                }
            }
        }

        override func tableView(tableView: UITableView, willBeginEditingRowAtIndexPath indexPath: NSIndexPath)
        {
            singleEdit = true
        }

        override func tableView(tableView: UITableView, didEndEditingRowAtIndexPath indexPath: NSIndexPath)
        {
            singleEdit = false
        }

        // MARK: - Navigation
        override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
        {
            if segue.identifier == "AddSpeaker"
            {
                let navigationController = segue.destinationViewController as UINavigationController
                let controller = navigationController.topViewController as AddSpeakerViewController
                controller.managedObjectContext = managedObjectContext
            }
        }
    }

    extension SpeakersViewController: NSFetchedResultsControllerDelegate
    {
        func controllerWillChangeContent(controller: NSFetchedResultsController)
        {
            tableView.beginUpdates()
        }

        func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
        {
            if indexPath?.section == 0
            {
                switch type
                {
                case .Insert:
                    tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
                case .Delete:
                    tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
                case .Update:
                    let sectionInfo = fetchedResultsController.sections![0] as NSFetchedResultsSectionInfo

                    if indexPath?.row < sectionInfo.numberOfObjects
                    {
                        let cell = tableView.cellForRowAtIndexPath(indexPath!) as SpeakerCell
                        let speaker = controller.objectAtIndexPath(indexPath!) as Speaker
                        cell.configureForSpeaker(speaker)
                    }
                case .Move:
                    tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
                    tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
                }
            }
        }

        func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType)
        {
            if sectionIndex == 0
            {
                switch type
                {
                case .Insert:
                    tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
                case .Delete:
                    tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
                case .Update:
                    break
                case .Move:
                    break
                }
            }
        }

        func controllerDidChangeContent(controller: NSFetchedResultsController)
        {
            tableView.endUpdates()
        }
    }

AddSpeakerViewController:

class AddSpeakerViewController: UITableViewController
{
    var managedObjectContext: NSManagedObjectContext!

    @IBOutlet weak var speakerFirstNameTextField: UITextField!
    @IBOutlet weak var speakerLastNameTextField: UITextField!

    override func viewDidLoad()
    {
        super.viewDidLoad()
    }
    override func didReceiveMemoryWarning()
    {
        super.didReceiveMemoryWarning()
    }
    @IBAction func cancelTapped(sender: AnyObject)
    {
        dismissViewControllerAnimated(true, completion: nil)
    }

    @IBAction func saveTapped(sender: AnyObject)
    {
        let speaker = NSEntityDescription.insertNewObjectForEntityForName("Speaker", inManagedObjectContext: managedObjectContext) as Speaker

        speaker.firstName = speakerFirstNameTextField.text

        var error: NSError?
        if !managedObjectContext.save(&error)
        {
            println("Error: \(error)")
            abort()
        }
        dismissViewControllerAnimated(true, completion: nil)
    }
}

Solution

  • Well, after some more hours of trying and going and testing every single line, I finally found out what was causing the problem.

    Checking in

    controller(didChangeObject) 
    

    for

    indexPath?.section == 0
    

    caused the problem. I don't really know why... But at least it works.