Search code examples
uitableviewcore-dataswift2nsfetchedresultscontroller

How to call didChangeSection to force update of sections in swift


I have been having a problem with CoreData and Sections from related entities. To show my issue, I have made as simple as possible version of my app. This is the link to the file if you'd like to download it. https://www.dropbox.com/s/y7gcpu7qq2mnrye/Sample%20List%20App.zip?dl=0

It's just a tableview, and hitting the plus button adds in a new item where the the quantity is 1, the name is 1, and the section is 1. Then clicking the cell increments them all. You can see that both the name and quantity are updating just fine, but the section never updates. This seems to be because the frc isn't tracking the change in the Section Table, but doesn't have a problem with the catalog or item table. When I quit the app and then start it up again, then the sections load correctly. Here are my current entities and relationships (of simple app)

enter image description here

Below is the code from the tableviewcontroller.

import UIKit
import CoreData

class TableViewController: UITableViewController, NSFetchedResultsControllerDelegate {

// MARK: - Constants and Variables

let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var frc: NSFetchedResultsController = NSFetchedResultsController()

// MARK: - App loading Functions

override func viewDidLoad() {
    super.viewDidLoad()

    frc = getFCR()
    frc.delegate = self

    do {
        try frc.performFetch()
    } catch {
        print("Failed to perform inital fetch")
    }
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

// MARK: - Outlets and Actions

@IBAction func addItemPress(sender: UIBarButtonItem) {

    var entityDesc = NSEntityDescription.entityForName("Items", inManagedObjectContext: self.moc)
    let item = Items(entity: entityDesc!, insertIntoManagedObjectContext: self.moc)

    item.qty = 1

    entityDesc = NSEntityDescription.entityForName("Catalog", inManagedObjectContext: self.moc)
    let catalog = Catalog(entity: entityDesc!, insertIntoManagedObjectContext: self.moc)

    catalog.name = 1

    var section: Sections?
    if (checkSectionName(0, moc: self.moc) == false) {
        entityDesc = NSEntityDescription.entityForName("Sections", inManagedObjectContext: self.moc)
        section = Sections(entity: entityDesc!, insertIntoManagedObjectContext: self.moc)

        section!.section = 1
    } else {
        section = returnSection(0, moc: self.moc)
    }

    item.catalog = catalog
    item.catalog!.sections = section

    do {
        try moc.save()
    } catch {
        fatalError("New item save failed")
    }
}

// MARK: - Table view data source

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {

    if let sections = frc.sections {
        return sections.count
    }

    return 0
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    if let sections = frc.sections {
        let currentSection = sections[section]
        return currentSection.numberOfObjects
    }

    return 0
}

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

    let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! TableViewCell
    let item: Items = frc.objectAtIndexPath(indexPath) as! Items

    cell.nameLbl.text = "Item #\(item.catalog!.name!)"
    cell.qtyLbl.text = "Qty: \(item.qty!.stringValue)"

    return cell
}

override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {

    if let sections = frc.sections {
        let currentSection = sections[section]
        return "Section \(currentSection.name)"
    }

    return nil
}

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.moveRowAtIndexPath(indexPath!, toIndexPath: newIndexPath!)
    }
}

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

    let item: Items = frc.objectAtIndexPath(indexPath) as! Items
    var qty: Int = Int(item.qty!)
    qty = qty + 1

    item.qty = qty

    var name: Int = Int(item.catalog!.name!)
    name = name + 1

    item.catalog!.name = name

    var sec: Int = Int(item.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)
    }

    item.catalog!.sections = section

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

}

func fetchRequest() -> NSFetchRequest {

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

    return fetchRequest

}

func getFCR() -> NSFetchedResultsController {

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

    return frc

}

func checkSectionName(sectionName: NSNumber, moc: NSManagedObjectContext) -> Bool {

    var exists: Bool = false

    let fetchReq = NSFetchRequest(entityName: "Sections")
    let pred = NSPredicate(format: "section == %@", sectionName)
    fetchReq.predicate = pred

    do {
        let check = try moc.executeFetchRequest(fetchReq)
        for rec in check {
            if let name = rec.valueForKey("section") {
                if (name as! NSNumber == sectionName) {
                    exists = true
                }
            }
        }
    } catch {
        fatalError("Failed fetching records when checking if List name already exists")
    }

    return exists

}

func returnSection(sectionName: NSNumber, moc: NSManagedObjectContext) -> Sections {

    let fetchReq = NSFetchRequest(entityName: "Sections")
    let pred = NSPredicate(format: "section == %@", sectionName)
    fetchReq.predicate = pred

    do {
        let check = try moc.executeFetchRequest(fetchReq)
        return check.first! as! Sections
    } catch {
        fatalError("Failed fetching records to return section")
    }
}

}

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.