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)
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")
}
}
}
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.