更改托管对象属性不会触发 NSFetchedResultsController 更新表视图

Posted

技术标签:

【中文标题】更改托管对象属性不会触发 NSFetchedResultsController 更新表视图【英文标题】:Changing a managed object property doesn't trigger NSFetchedResultsController to update the table view 【发布时间】:2012-09-11 13:11:56 【问题描述】:

我有一个带有谓词的 fetchedResultsController,其中 "isOpen == YES"

当调用 closeCurrentClockSet 时,我将该属性设置为 NO。因此,它不应再出现在我的 tableView 上。

出于某种原因,这不会发生。

有人可以帮我解决这个问题吗?

-(void)closeCurrentClockSet


    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"isOpen == YES"];

    NSArray *fetchedObjects =
        [self fetchRequestForEntity:@"ClockSet"
                      withPredicate:predicate
             inManagedObjectContext:[myAppDelegate managedObjectContext]];

    ClockSet *currentClockSet = (ClockSet *)fetchedObjects.lastObject;

    [currentClockSet setIsOpen:[NSNumber numberWithBool:NO]];


--

我还有几个方法,使用完全相同的方法, 通过调用自定义 fetchRequestForEntity:withPredicate:inManagedObjectContext 方法。

在这些方法中,当更改属性时,tableView 会正确更新! 但是上面的这个(closeCurrentClockSet),没有!我不知道为什么。

--

我的 fetchedResultsController 实现来自 Apple 的文档。

另外,还有一个细节。如果我将我的应用程序发送到后台。关闭并重新打开,tableView 显示更新!

我已尽力在 *** 上关注之前的问题。没运气。我也 NSLogged 这一点。 对象被正确获取。这是正确的。 isOpen 属性 已正确更新为 NO。但由于某种原因,我的 fetchedResultsController 没有更新 tableView。

我确实尝试了几个“锤子”解决方案,例如 reloadData 和调用 performFetch。但这没有用。或者使用它们会有意义......

编辑:从头开始,它确实有效,在我的 resultsController 上的 performFetch 之后立即调用 reloadData 但使用 reloadData 正在敲定解决方案。另外,它会删除所有动画。我希望我的控制器自动更新我的 tableView。

有人可以帮我解决这个问题吗?

非常感谢任何帮助!

谢谢,

努诺

编辑:

完整的实现。

fetchedResultsController 非常标准和简单。其他一切都来自 Apple 的文档

- (NSFetchedResultsController *)fetchedResultsController


    if (_fetchedResultsController) 
        return _fetchedResultsController;
    

    NSManagedObjectContext * managedObjectContext = [myAppDelegate managedObjectContext];

    NSEntityDescription *entity  =
        [NSEntityDescription entityForName:@"ClockPair"
                    inManagedObjectContext:managedObjectContext];

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
        [fetchRequest setEntity:entity];

    NSString *predicate = [NSString stringWithFormat: @"clockSet.isOpen == YES"];
        [fetchRequest setPredicate: [NSPredicate predicateWithFormat:predicate]];

    NSSortDescriptor *sortDescriptor1 =
        [[NSSortDescriptor alloc] initWithKey:@"clockIn" ascending:NO];

    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, nil];

        [fetchRequest setSortDescriptors:sortDescriptors];
        [fetchRequest setFetchBatchSize:20];

    NSFetchedResultsController *theFetchedResultsController =
        [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                            managedObjectContext:managedObjectContext
                                              sectionNameKeyPath:nil
                                                       cacheName:@"Root"];


    _fetchedResultsController = theFetchedResultsController;
    _fetchedResultsController.delegate = self;

    return _fetchedResultsController;


--

Apple 文档中的样板代码:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller

    // The fetch controller is about to start sending change notifications, so prepare the table view for updates.
    [self.tableView beginUpdates];




- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath


    UITableView *tableView = self.tableView;

    switch(type) 

        case NSFetchedResultsChangeInsert:

            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                             withRowAnimation:UITableViewRowAnimationTop];

            break;

        case NSFetchedResultsChangeDelete:

            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                             withRowAnimation:UITableViewRowAnimationFade];

            break;

        case NSFetchedResultsChangeUpdate:

            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                             withRowAnimation:UITableViewRowAnimationFade];

            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                             withRowAnimation:UITableViewRowAnimationFade];

            break;

        case NSFetchedResultsChangeMove:

            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                             withRowAnimation:UITableViewRowAnimationLeft];

            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                             withRowAnimation:UITableViewRowAnimationTop];

            break;
    




- (void)controller:(NSFetchedResultsController *)controller
  didChangeSection:(id )sectionInfo
           atIndex:(NSUInteger)sectionIndex
     forChangeType:(NSFetchedResultsChangeType)type


    UITableView *tableView = self.tableView;

    switch(type) 

        case NSFetchedResultsChangeInsert:

            [tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                     withRowAnimation:UITableViewRowAnimationFade];

            break;

        case NSFetchedResultsChangeDelete:

            [tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                     withRowAnimation:UITableViewRowAnimationFade];

            break;
    




- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller

    // The fetch controller has sent all current change notifications, so tell the table view to process all updates.
    [self.tableView endUpdates];

第一次更新:

Tracking [managedObjectContext hasChanges] 确实返回 YES,这是应该的。但是 fetchedResultsController 不会更新 tableView

第二次更新

didChangeObject:atIndexPath: 对于这种特殊情况,不会被调用! 我还有 2 种方法,使用完全相同的代码,它们只是碰巧是不同的实体。他们完美地工作。感谢@Leonardo 指出这一点

3TH UPDATE这个方法,遵循同样的规则。但确实有效。

- (void)clockOut

    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"isOpen == %@", [NSNumber numberWithBool:YES]];

    NSArray * fetchedObjects =
        [self fetchRequestForEntity:@"ClockPair"
                      withPredicate:predicate
             inManagedObjectContext:[myAppDelegate managedObjectContext]];

    ClockPair *aClockPair = (ClockPair *)fetchedObjects.lastObject;

    aClockPair.clockOut = [NSDate date];
    aClockPair.isOpen   = [NSNumber numberWithBool:NO];



有人对我可能遗漏的内容有任何其他想法吗?

谢谢,

努诺

【问题讨论】:

设置为 NO 后,您是否尝试保存上下文?我想是的。您是否还尝试查看 _fetchedResultsController 是否仍将控制器作为委托?您是否设置了断点以查看是否输入了 didChangeObject:atIndexPath: ?另外,我不确定这一点,但我认为谓词不应该用字符串构建,而是直接用 [NSPredicate predicateWithFormat:@"isOpen==%@",[NSNumber numberWithBool:NO]] 我做到了。它确实保存了应有的上下文,而不会产生任何错误。是的,它仍然存在。对 -> other 这是因为您通过关系进行过滤的方式。关系中的数据发生了变化,但 FRC 已经获取了这些对象。为了确认,请发布更改数据库但“工作正常”的其他方法之一的代码。另外,您说您尝试重新加载和重新获取,但没有成功。这很奇怪,所以请发布您为重新加载/重新获取数据所做的工作。 刚刚发布了更新 3。我花了一整天的时间...然后你来了!它现在有效......我很难过!非常感谢先生!调用 performFetch 后 reloadData。 "if (![[self fetchedResultsController] performFetch:&error])" 现在可以工作了!但我不得不说,我真的很想在不调用 reloadData 的情况下做到这一点。 reloadData 取出我的动画。你认为没有它我还能做到这一点吗? @JodyHagins 您还说过,“关系中的数据发生了变化,但 FRC 已经获取了这些对象”。我很想明白这一点。它确实获取了对象。但是通过将该属性更改为“否”,它们应该不再出现。因为我的谓词过滤掉了它们。 p.s. ClockSet 有很多 Clo​​ckPairs。如果clockSet.isOpen 设置为NO。然后所有clockPairs 应该从我的tableView 中消失。我不需要强制手动 performFetch,重新获取,然后重新加载数据。我应该吗? 【参考方案1】:

好的,我会解释你的问题,然后我会让你判断它是否是FRC中的错误。如果您认为这是一个错误,那么您真的应该向苹果提交错误报告。

你的获取结果控制器谓词是这样的:

NSString *predicate = [NSString stringWithFormat: @"clockSet.isOpen == YES"];

这是一个布尔值的有效谓词。它将遵循clockSet 实体的关系并获取其isOpen 属性。如果是YES,那么这些对象将被接受到对象数组中。

我认为我们在这里做得很好。

现在,如果您将clockSet.isOpen 属性之一更改为NO,那么您希望看到该对象从您的表视图中消失(即,它不应再与谓词匹配,因此应从获取的对象)。

所以,如果你有这个......

[currentClockSet setIsOpen:[NSNumber numberWithBool:NO]];

那么,无论哪个***对象与 currentClockSet 有关系,都应该从您的 FRC 获取结果数组中“消失”。

但是,您并没有看到它消失。原因是FRC监控的对象没有改变。是的,谓词键路径已更改,但 FRC 包含 ClockPairClockSet 实体实际上已更改。

您可以观看通知飞来飞去,了解幕后发生的事情。

无论如何,FRC 将在您进行提取时使用键路径,但它不会监视对不在其实际提取对象集中的对象的更改。

最简单的解决方法是为保存此关键路径对象的对象“设置”一个属性。

例如,我注意到ClockPair 也有一个isOpen 属性。如果你有一个反向关系,那么你可以这样做......

currentClockSet.isOpen = NO;
currentClockSet.clockPair.isOpen = currentClockSet.clockPair.isOpen;

请注意,您实际上并没有更改该值。然而,setter 被调用,触发了 KVO,因此触发了私有 DidChange 通知,然后通知 FRC 对象已更改。因此,它会重新评估检查以查看是否应包含该对象,发现 keypath 值已更改,然后执行您期望的操作。

因此,如果您在 FRC 谓词中使用键路径,如果您更改了该值,则需要以您的方式返回 FRC 数组中的所有对象并“弄脏它们”,以便这些对象位于传递有关对象更改的通知。这很丑陋,但可能比保存或更改您的获取请求并重新获取更好。

我知道你不相信我,所以继续尝试吧。请注意,要使其正常工作,您必须知道 FRC 对象数组中的哪些项目会受到更改的影响,并“戳”它们以使 FRC 注意到更改。

正如我之前提到的,另一个选项是保存上下文,然后重新获取值。如果您不想保存上下文,您可以让 fetch 包含当前上下文中的更新,而无需从存储中刷新。

我发现伪造 FRC 正在监视的对象的更改是完成对作为其他实体的关键路径的谓词进行重新评估的最佳方式。

好的,所以,这是否是一个错误还有待商榷。就我个人而言,我认为如果 FRC 要监控密钥路径,它应该全程监控,而不是像我们在这里看到的那样部分监控。

我希望这是有道理的,我鼓励您提交错误报告。

【讨论】:

我从来没有为此感谢你。谢谢你,先生。半年后,你说的一切现在对我来说都是有意义的,尽管当时有点混乱。是的,我也觉得它很丑,但如果我没记错的话,它确实解决了这个问题。谢谢! 我想为此亲吻你的脸。我不得不更改我的谓词,使其包含封装集合中所有对象的关系加上“property == %@”,@YES 谓词以进一步过滤。然后,当我将属性从 NO 翻转为 YES 时,我的获取结果控制器会收到来自 MOC-did-save 通知的更新。 是否有提交过错误报告?这看起来已经有几年历史了,是否有其他解决方案可以强制 FRC“查看”对 keyPath 所做的更改? 如果您的问题不同,请创建一个新问题。请务必包含所有必要的代码来重现您的问题。 天堂!这是您认为“你到底是怎么发现这个的,伙计?!”的答案之一。你有我最崇高的敬意,也谢谢你。不错。【参考方案2】:

您遇到了类似的问题。

我知道这个问题已经很老了,但我希望这对其他人有帮助:

最简单的方法是在父对象中引入一个名为lastUpdated: NSDate 的新属性。

我有一个Conversation,其中包含几个Messages。每当更新消息的isRead 标志时,我需要更新ConversationOverviewViewController,它只显示Conversations。此外,ConversationOverviewVC 中的 NSFetchedResultsController 仅获取 Conversations 并且对 Message 一无所知。

每当有消息更新时,我都会致电message.parentConversation.lastUpdated = NSDate()。这是手动触发更新的一种简单而有用的方法。

希望这会有所帮助。

【讨论】:

【参考方案3】:

[currentClockSet setIsOpen:[NSNumber numberWithBool:NO]];之后可以保存托管对象上下文吗:

NSError *saveError = nil;
if( ![[myAppDelegate managedObjectContext] save:&saveError] ) 
    // handle error saving context

我怀疑您的UITableView 在保存上下文后会正确更新。这可能就是为什么将您的应用程序发送到后台有效的原因。我怀疑您的核心数据堆栈是在应用程序的委托中设置的,当它进入后台时,它会在主 NSManagedObjectContext 上执行保存。

【讨论】:

那里没有成功。这个念头确实闪过我的脑海。并且不会产生错误。奇怪的是,去后台还不足以更新它。它必须关闭然后重新打开。只有在重新打开时,tableView 才能获得正确的结果。进入后台和终止应用程序都调用 saveContext。这几乎就是他们所做的一切。感谢您的建议@Alan。我真的很感激。

以上是关于更改托管对象属性不会触发 NSFetchedResultsController 更新表视图的主要内容,如果未能解决你的问题,请参考以下文章

Vue 组件 prop 更改不会触发重新渲染

iOS:托管对象关系的属性已更改,但托管对象没有注意到

触发的数据触发器不会更改自定义控件的新添加属性

在 NSFetchedResultsController 中更改托管对象的属性

事件属性更改不会触发

在 Core Data 中更改托管对象模型的属性值时崩溃