Search code examples
swiftcore-datansfetchedresultscontrollerentity-relationship

Section update event not being called from related entity in swift


Below is a link to download a simplified version of my app that has the exact same problem. The plus "Add" button at the top adds a new record that is set at name = 1, qty = 1, and section = 1. Selecting a Cell increments them all to the next number. You can see that both the name and qty update, but the section never updates until you quit the app and start it again. DropBox Download Link

I have the following relationship setup in CoreData:

enter image description here

In in my TableViewController, I am creating my FetchRequestController (frc) with the following code:

func fetchRequest() -> NSFetchRequest {

    let fetchRequest = NSFetchRequest(entityName: "Items")
    let sortDesc1 = NSSortDescriptor(key: "catalog.sections.section", ascending: true)
    let sortDesc2 = NSSortDescriptor(key: "isChecked", ascending: true)
    let sortDesc3 = NSSortDescriptor(key: "catalog.name", ascending: true)
    fetchRequest.sortDescriptors = [sortDesc1, sortDesc2, sortDesc3]

    return fetchRequest

}

func getFCR() -> NSFetchedResultsController {

    frc = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: moc, sectionNameKeyPath: "catalog.sections.section" , cacheName: nil)

    return frc

}

So as shown, I'm preforming the fetch request on the Item Entity, and sorting by attributes in both the Catalog and Sections entities. And specifically to my problem, I have the sections key in my frc as the section attribute in the Sections Entity (which is related through the Catalog Entity).

When I'm updating various parts of the Item or Catalog I see the table cell update correctly (i.e. the didChangeObject event is called)

But if I change the section it never updates unless I completely back out of the table and then reenter it. (i.e. the didChangeSection event is never called even though the section is changing)

Below is the code I'm using to edit a pre-existing Item Record.

func editItem() {

    let item: Items = self.item!

    item.qty = Float(itemQty.text!)

    item.catalog!.name = Util.trimSpaces(itemName.text!)
    item.catalog!.brand = Util.trimSpaces(itemBrand.text!)
    item.catalog!.qty = Float(itemQty.text!)
    item.catalog!.price = Util.currencyToFloat(itemPrice.text!)
    item.catalog!.size = Util.trimSpaces(itemSize.text!)
    item.catalog!.image = UIImageJPEGRepresentation(itemImage.image!, 1)

    if (self.section != nil) {
        item.catalog!.sections = self.section
    }

    do {
        try moc.save()
    } catch {
        fatalError("Edit Item save failed")
    }

    if (itemProtocal != nil) {
        itemProtocal!.finishedEdittingItem(self, item: self.item!)
    }

}

Just to note, when I add in a new record into the Item Entity, the didChangeObject and didChangeSection events are both properly called. Only when editing them is didChangeSection getting skipped or missed.

Just for completion, below is my code I'm using for didChangeObject and didChangeSection.

func controllerWillChangeContent(controller: NSFetchedResultsController) {

    tableView.beginUpdates()

}

func controllerDidChangeContent(controller: NSFetchedResultsController) {

    tableView.endUpdates()

}

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

    switch type {
    case NSFetchedResultsChangeType.Update:
        self.tableView.reloadSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
    case NSFetchedResultsChangeType.Delete:
        self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
    case NSFetchedResultsChangeType.Insert:
        self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
    case NSFetchedResultsChangeType.Move:
        self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
        self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
    }

}

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

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

}

When I googled this issue, I found that others have had problems similar to this and it seems to be a feature (or bug) of the frc and how Xcode handles relationships. Basically, the frc is only watching the Item Entity, and when the Section Entity changes, it doesn't register with the frc. People have suggested various hacks as well, but so far none of them seem to be working for me. Examples are to do something like this item.catalog.sections = item.catalog.sections None of the examples had the section key as a related entity, so I'm not sure if that is why they aren't working for me.

So my question is if is there some way to tell didChangeSection to execute and send it the proper NSFetchedResultsChangeType? Or even better yet, is there some way to "encourage" the frc to notice what is happening in the Section Entity that is related to the Item Entity through the Catalog Entity.


Solution

  • After playing with this a little, it seems the didChangeSection is only fired if the first relationship named in the sectionNameKeyPath is directly modified (ie. in this case, if you create a new Catalog linked to the correct section, and set item.catalog = newCatalog). But I think that is too convoluted as a work-around.

    One solution would be to change your FRC to fetch the Catalog objects instead of Items. Since they map one-one, the table view should retain the same structure. The key changes are:

    func fetchRequest() -> NSFetchRequest {
    
        let fetchRequest = NSFetchRequest(entityName: "Catalog")
        let sortDesc1 = NSSortDescriptor(key: "sections.section", ascending: true)
        let sortDesc2 = NSSortDescriptor(key: "name", ascending: true)
        fetchRequest.sortDescriptors = [sortDesc1, sortDesc2]
    
        return fetchRequest
    }
    

    and

    func getFCR() -> NSFetchedResultsController {
    
        frc = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: moc, sectionNameKeyPath: "sections.section" , cacheName: nil)
    
        return frc
    
    }
    

    Then modify the references to frc to reflect this change:

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    
        let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! TableViewCell
        let catalog: Catalog = frc.objectAtIndexPath(indexPath) as! Catalog
    
        cell.nameLbl.text = "Item #\(catalog.name!)"
        cell.qtyLbl.text = "Qty: \(catalog.items.qty!.stringValue)"
    
        return cell
    }
    

    and

    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    
        let catalog: Catalog = frc.objectAtIndexPath(indexPath) as! Catalog
    
        var qty: Int = Int(catalog.items.qty!)
        qty = qty + 1    
        catalog.items.qty = qty
    
        var name: Int = Int(catalog.name!)
        name = name + 1
        catalog.name = name
    
        var sec: Int = Int(catalog.sections.section!)
        sec = sec + 1
        var section: Sections?
        if (checkSectionName(sec, moc: self.moc) == false) {
            let entityDesc = NSEntityDescription.entityForName("Sections", inManagedObjectContext: self.moc)
            section = Sections(entity: entityDesc!, insertIntoManagedObjectContext: self.moc)
    
            section!.section = sec
        } else {
            section = returnSection(sec, moc: self.moc)
        }
    
        catalog.sections = section
    
        do {
            try moc.save()
        } catch {
            fatalError("Edit item save failed")
        }
    
    }
    

    Because you are directly modifying the sections property of the catalog object, this will trigger the didChangeSection method. This still feels to me like a bit of a hack, but since the FRC is not behaving as one would like, a hack might be a necessary evil.