Search code examples
iosswiftcore-datansfetchedresultscontroller

Two NSFetchResultControllers , One TableViewController


I want to create a tableView with two sections. Each section will be filled from a different NSFetchResultController. I have one entity in my database called "newsItem". In the first section i want to display the first five newsItems with a an attribute called "main_article" equal to true. The second section will be all newsItems.

This is my NSFetchResultControllers

 private lazy var fetchedResultsController: NSFetchedResultsController? = { [unowned self] in
    let fetchRequest = NSFetchRequest(entityName: "NewsItem")
    let context = self.managedObjectContext
    fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
    fetchRequest.predicate = NSPredicate(format: "main_article = YES")
    fetchRequest.fetchLimit = 5
    let controller = NSFetchedResultsController(fetchRequest: fetchRequest,managedObjectContext: context,sectionNameKeyPath: nil,cacheName: nil)

    controller.delegate = self
    controller.performFetch(nil)

    return controller
}()



private lazy var newsroomFetchedResultsController: NSFetchedResultsController? = { [unowned self] in
    let fetchRequest = NSFetchRequest(entityName: "NewsItem")
    let context = self.managedObjectContext
    fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
    fetchRequest.fetchBatchSize = 100
    let controller = NSFetchedResultsController(fetchRequest: fetchRequest,managedObjectContext: context,sectionNameKeyPath: nil,cacheName: nil)
    controller.delegate = self
    controller.performFetch(nil)

    return controller
}()

NSFetchResultController Delegate Methods

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


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

    switch type {
    case .Insert:
        self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Automatic)
    case .Update:
        self.tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: .Automatic)
    case .Move:
        self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Automatic)
        self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Automatic)
    case .Delete:
        self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Automatic)
    default:
        return
    }

}

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

TableView Delegate Methods

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 2
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if section == 0 {
            return fetchedResultsController?.fetchedObjects?.count ?? 0
        }else {
            return newsroomFetchedResultsController?.fetchedObjects?.count ?? 0
        }


    }

    func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        if section == 1 {
            return (tableView.dequeueReusableCellWithIdentifier("section header") as? UITableViewCell)
        }

        return nil
    }

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

            if let newItem = fetchedResultsController?.fetchedObjects?[indexPath.row] as? NewsItem  where indexPath.section == 0 {

                    if let cell = tableView.dequeueReusableCellWithIdentifier("main cell") as? NewMainTableViewCell where indexPath.row == 0{
                        cell.lblTitle.text = newItem.title
                        return cell

                    }
                    else if let cell = tableView.dequeueReusableCellWithIdentifier("new cell") as? NewTableViewCell{
                            cell.lblTitle.text = newItem.title
                            cell.backgroundColor = UIColor.blackColor()
                            cell.lblTitle.textColor = UIColor.whiteColor()
                            return cell
                    }


            }
            else if let newItem = newsroomFetchedResultsController?.fetchedObjects?[indexPath.row] as? NewsItem where indexPath.section == 1 {
                if let cell = tableView.dequeueReusableCellWithIdentifier("new cell") as? NewTableViewCell {
                    cell.lblTitle.text = newItem.title
                    cell.backgroundColor = UIColor.whiteColor()
                    cell.lblTitle.textColor = UIColor.blackColor()
                    return cell
                }
            }


        return UITableViewCell()
    }

    func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        if section == 1 {
            return 30
        }

        return 0
    }

    func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return 0.001
    }

    func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        return UIView(frame: CGRectZero)
    }

    func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        if indexPath.section == 0 && indexPath.row == 0 {
            return UIScreen.mainScreen().bounds.width * 1.1
        }else {
            return 67
        }
    }

When i am trying to run it i get this error and tableview appears empty :

*2015-06-26 15:33:29.183 Kathimerini[5023:965439] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-3347.44/UITableView.m:1623*

2015-06-26 15:33:29.183 Kathimerini[5023:965439] CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (5) must be equal to the number of rows contained in that section before the update (5), plus or minus the number of rows inserted or deleted from that section (1 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)


Solution

  • Your delegate methods have to take into account both fetched results controllers. In the delegate callback to insert/delete etc. rows, you should first check

    if controller == self.fetchedResultsController {
       // modify section 1
    }
    else {
       // modify section 0
    }
    

    You can test if your setup works as expected upfront by first not setting the delegate of the fetched results controllers.

    Also, make sure you understand the meaning of the index paths. The main FRC will report e.g. a new item to be inserted at index path (0,10) but you have to insert it at (1,10). The FRC does not know about your sections because that is an implementation detail of your table view.