Search code examples
iosswiftuitableviewnsfetchedresultscontroller

How to use NSFetchedResultsController to add a section (with a custom header cells) in a table view controller?


I'm making a table view that has an expandable table with the sections (bunches) being a custom UITableViewCell as well as the rows (buddies) -- you can make arbitrary bunches of buddies. I'm also, populating the table using a NSFetchedResultsController, which I have successfully done. The difficulty I'm having is when adding a bunch to core data, the NSFetchedResultsController throws this exception:

CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. attempt to insert row 0 into section 1, but there are only 0 rows in section 1 after the update with userInfo (null)

I would like to be able to add a bunch (in a modal dialog) and have it automatically show up in the table view (per NSFetchedResultController capabilities), but it throws an exception (doesn't crash though) as seen above and the section is not added.

Here is the code for the NSFetchedResultsController:

Initialization (initialized on load of the table view)

lazy var fetchedResultsController: NSFetchedResultsController = {
    let bunchesFetchRequest = NSFetchRequest(entityName: Constants.CoreData.bunch)
    // Sort bunches by bunch name, in alphabetical order, not caring about capitalization
    let primarySortDescriptor = NSSortDescriptor(key: Constants.CoreData.Bunch.name, ascending: true, selector: "caseInsensitiveCompare:")
    bunchesFetchRequest.sortDescriptors = [primarySortDescriptor]

    // TODO: Answer question: Do we need this, does this benefit us at all, and how does prefetching work?
    bunchesFetchRequest.relationshipKeyPathsForPrefetching = [Constants.CoreData.Bunch.buddies]

    let frc = NSFetchedResultsController(
        fetchRequest: bunchesFetchRequest,
        managedObjectContext: CoreDataStackManager.sharedInstance().managedObjectContext!,
        sectionNameKeyPath: Constants.CoreData.Bunch.name,
        cacheName: nil
    )
    frc.delegate = self
    return frc
}()

Table View Methods

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    // sections are the bunches
    if let sections = fetchedResultsController.sections {
        return sections.count
    }
    return 0
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // sections are the bunches (this is for when sectionNameKeyPath is set)
    if let sections = fetchedResultsController.sections {
        let currentSection = sections[section] as! NSFetchedResultsSectionInfo
        let bunch = currentSection.objects[0] as! Bunch
        // Return the number of buddies in this section (bunch)
        return bunch.buddies.count
    }
    return 0
}


func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

    if let sections = fetchedResultsController.sections {
        let currentSection = sections[section] as! NSFetchedResultsSectionInfo
        let bunch = currentSection.objects[0] as! Bunch
            // Create BunchTableViewCell
        let headerCell: BunchTableViewCell = tableView.dequeueReusableCellWithIdentifier(bunchCellIdentifier) as! BunchTableViewCell

        headerCell.bunchNameLabel.text = bunch.name    
        return headerCell
    }
    return nil
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell: BuddyInBunchTableViewCell = tableView.dequeueReusableCellWithIdentifier(buddyInBunchCellIdentifier, forIndexPath: indexPath) as! BuddyInBunchTableViewCell

    if let sections = fetchedResultsController.sections {
        let currentSection = sections[indexPath.section] as! NSFetchedResultsSectionInfo
        let bunch = currentSection.objects[0] as! Bunch
        let buddy: Buddy = bunch.getBuddiesInBunch()[indexPath.row]
        cell.buddyFullNameLabel.text = buddy.getFullName()
    }
    return cell
}

func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return 50.0 
}

NSFetchedResultsController Methods

func controllerWillChangeContent(controller: NSFetchedResultsController) {
    self.tableView.beginUpdates()
}

func controller(
    controller: NSFetchedResultsController,
    didChangeObject anObject: AnyObject,
    atIndexPath indexPath: NSIndexPath?,
    forChangeType type: NSFetchedResultsChangeType,
    newIndexPath: NSIndexPath?) {

        switch type {
        case NSFetchedResultsChangeType.Insert:
            // Note that for Insert, we insert a row at the __newIndexPath__
            if let insertIndexPath = newIndexPath {
                // AAAAAAAAAA
                self.tableView.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
                // BBBBBBBBBB
                // self.tableView.insertSections(NSIndexSet(index: insertIndexPath.section), withRowAnimation: UITableViewRowAnimation.Fade)
            }
        case NSFetchedResultsChangeType.Delete:
            // Note that for Delete, we delete the row at __indexPath__
            if let deleteIndexPath = indexPath {
                self.tableView.deleteRowsAtIndexPaths([deleteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
            }
        case NSFetchedResultsChangeType.Update:
            // Note that for Update, we update the row at __indexPath__
            // Not yet implemented
            break
        case NSFetchedResultsChangeType.Move:
            // Note that for Move, we delete the row at __indexPath__
            if let deleteIndexPath = indexPath {
                self.tableView.deleteRowsAtIndexPaths([deleteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
            }

            // Note that for Move, we insert a row at the __newIndexPath__
            if let insertIndexPath = newIndexPath {
                self.tableView.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
            }
        }
}


func controller(
    controller: NSFetchedResultsController,
    didChangeSection sectionInfo: NSFetchedResultsSectionInfo,
    atIndex sectionIndex: Int,
    forChangeType type: NSFetchedResultsChangeType) {

        switch type {
        case .Insert:
            // AAAAAAAAAA
            let sectionIndexSet = NSIndexSet(index: sectionIndex)
            self.tableView.insertSections(sectionIndexSet, withRowAnimation: UITableViewRowAnimation.Fade)
            // BBBBBBBBBBB
            // break
        case .Delete:
            let sectionIndexSet = NSIndexSet(index: sectionIndex)
            self.tableView.deleteSections(sectionIndexSet, withRowAnimation: UITableViewRowAnimation.Fade)
        default:
            break
        }
}

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

Note: if I comment out the lines under AAAAAAAAAA and uncomment the lines in BBBBBBBBBB, I can see the cell appear very briefly, but then every cell below that inserted cell disappears and I get this error instead many times:

no index path for table cell being reused

Any help/suggestion is appreciated!


Solution

  • Well, I found out that:

    "One of the bigger problems with NSFetchedResultsController is that it cannot show empty sections."

    That was mentioned in this blog post that mentions a way to get around it (Note: I have not yet implemented this, but I now know that it is not possible in the intuitive way).