问题
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:
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.
回答1:
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.
回答2:
I have run into a similar situation and I ended up creating a second NSFetchedResultsController
for the other type, and just listened to it for change events and updated my view as appropriate. Not an ideal solution, since it requires some manual coordination and keeping of some meta data, but it does get the job done.
回答3:
This is really an extension of the answer from @CharlesA
As you have found the FRC doesn't observe anything other than its associated entity for changes. This isn't a feature or a bug, it's an implementation detail because it covers a large percentage of usage and the code required to analyse the graph for arbitrary depth changes is complex. In addition to that the delegate callbacks for a single change merged into the context could be extremely complex and effectively impossible to apply to the table with animations.
So, you really need to change your approach. The answer from Charles using a second FRC requires that the second FRC is used to instruct the first FRC to re-execute it's fetch and the fully reload the table view. You need to re-execute the fetch because otherwise your data source is wrong and there's no other way to update it. Technically you could try to apply the changes to the table with animation, depending on what you know / guarantee about how the changes are made and saved to the context it could work. Like if the user can only change 1 section at a time and it's auto-saved. And that no compound changes are made on a background thread.
The answer from @pbasdf is also a suitable alternative, but it will still only deal with relationship changes, not changes to the values in the objects at the end of those relationships - this is a key distinction you need to understand and appreciate when using FRCs.
来源:https://stackoverflow.com/questions/37328774/section-update-event-not-being-called-from-related-entity-in-swift