swift 中的相关实体未调用部分更新事件

Section update event not being called from related entity in swift

下面是 link 下载我的应用程序的简化版本,它具有完全相同的问题。顶部的加号 "Add" 按钮添加一条新记录,该记录设置为名称 = 1、数量 = 1 和部分 = 1。选择一个单元格会将它们全部递增到下一个数字。您可以看到名称和数量都更新了,但是该部分永远不会更新,直到您退出应用程序并重新启动它。 DropBox Download Link

我在 CoreData 中设置了以下关系:

在我的 TableViewController 中,我正在使用以下代码创建我的 FetchRequestController (frc):

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

}

如图所示,我在项目实体上执行提取请求,并按目录和部分实体中的属性排序。特别是针对我的问题,我将我的 frc 中的 sections 键作为 Sections 实体中的 section 属性(通过目录实体相关)。

当我更新项目或目录的各个部分时,我看到 table 单元格更新正确(即调用了 didChangeObject 事件)

但是,如果我更改该部分,它永远不会更新,除非我完全退出 table 然后重新进入它。 (即 didChangeSection 事件永远不会被调用,即使该部分正在更改)

下面是我用来编辑预先存在的项目记录的代码。

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!)
    }

}

请注意,当我将新记录添加到项目实体时,didChangeObject 和 didChangeSection 事件都被正确调用。只有在编辑它们时才会跳过或错过 didChangeSection。

为了完成,下面是我用于 didChangeObject 和 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)
    }

}

当我搜索这个问题时,我发现其他人也有类似的问题,这似乎是 frc 的一个特性(或错误)以及 Xcode 如何处理关系。基本上,frc 只监视 Item Entity,当 Section Entity 发生变化时,它不会向 frc 注册。人们也提出了各种黑客建议,但到目前为止 none 似乎对我有用。例子是做这样的事情 item.catalog.sections = item.catalog.sections None 个示例将部分键作为相关实体,所以我不确定这是否就是它们不适合我的原因。

所以我的问题是,是否有某种方法可以告诉 didChangeSection 执行并向其发送正确的 NSFetchedResultsChangeType?或者更好的是,有没有什么方法可以 "encourage" frc 注意到通过目录实体与项目实体相关的部分实体中发生的事情。

我 运行 遇到了类似的情况,我最终为另一种类型创建了第二个 NSFetchedResultsController,只是听了它的变化事件并适当地更新了我的观点。这不是一个理想的解决方案,因为它需要一些手动协调和保留一些元数据,但它确实完成了工作。

玩了一会儿之后,似乎 didChangeSection 只有在 sectionNameKeyPath 中命名的第一个关系被直接修改时才会触发(即,在这种情况下,如果您创建一个新的Catalog 链接到正确的部分,并设置 item.catalog = newCatalog)。但我认为这作为解决方法太复杂了。

一个解决方案是更改您的 FRC 以获取 Catalog 个对象而不是 Items。由于它们一对一映射,因此 table 视图应保留相同的结构。主要变化是:

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
}

func getFCR() -> NSFetchedResultsController {

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

    return frc

}

然后修改对 frc 的引用以反映此更改:

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
}

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

}

因为你是直接修改catalog对象的sections属性,这样会触发didChangeSection方法。这对我来说仍然感觉有点像黑客攻击,但由于 FRC 的行为并不像人们希望的那样,黑客攻击可能是一种必要的邪恶。

这实际上是@CharlesA

答案的延伸

正如您所发现的,FRC 除了其关联的实体发生变化外,没有观察到任何其他信息。这不是一个特性或错误,它是一个实现细节,因为它涵盖了很大一部分使用情况,并且分析图形的任意深度变化所需的代码很复杂。除此之外,合并到上下文中的单个更改的委托回调可能非常复杂,实际上不可能应用于带有动画的 table。

所以,你真的需要改变你的方法。 Charles 使用第二个 FRC 的答案要求第二个 FRC 用于指示第一个 FRC 重新执行它的获取并完全重新加载 table 视图。您需要重新执行提取,否则您的数据源是错误的并且没有其他方法可以更新它。从技术上讲,您可以尝试通过动画将更改应用到 table,具体取决于您对如何进行更改以及如何将更改保存到它可以工作的上下文的了解/保证。就像用户一次只能更改 1 个部分并且自动保存一样。并且没有在后台线程上进行复合更改。

@pbasdf 的答案也是一个 suitable 替代方案,但它仍然只处理关系变化,而不是这些关系结束时对象中值的变化 - 这是关键使用 FRC 时需要理解和欣赏的区别。