Core data NSFetchedResultsController的NSFetchedResultsSchangeUpdate事件中的NSRangeException异常

Core data NSFetchedResultsController的NSFetchedResultsSchangeUpdate事件中的NSRangeException异常,core-data,nsfetchedresultscontroller,Core Data,Nsfetchedresultscontroller,我有一个UITableView,它使用NSFetchedResultsController作为数据源 核心数据存储在并行运行的多个后台线程中更新(每个线程使用自己的NSManagedObjectContext) 主线程观察NSManagedObjectContextDidSaveNotification 通知并使用mergeChangesFromContextDidSaveNotification:更新其上下文 有时,NSFetchedResultsController会发送 NSFetched

我有一个
UITableView
,它使用
NSFetchedResultsController
作为数据源

核心数据存储在并行运行的多个后台线程中更新(每个线程使用自己的
NSManagedObjectContext

主线程观察
NSManagedObjectContextDidSaveNotification
通知并使用
mergeChangesFromContextDidSaveNotification:
更新其上下文

有时,
NSFetchedResultsController
会发送
NSFetchedResultsChangeUpdate
事件的indexPath不存在 在那一点上再也没有了

例如:获取结果控制器的结果集包含 1节包含4个对象。在一个线程中删除第一个对象。 最后一个对象在另一个线程中更新。然后有时候 发生以下情况:

  • controllerWillChangeContent:被调用
  • 控制器:didChangeObject:atIndexPath:forChangeType:newIndexPath:通过调用 type=NSFetchedResultsChangeDelete,indexPath.row=0
  • 控制器:didChangeObject:atIndexPath:forChangeType:newIndexPath:通过调用 type=NSFetchedResultsChangeUpdate,indexPath.row=3
但是获取的结果控制器现在只包含3个对象,如果调用

MyManagedObject *obj = [controller objectAtIndexPath:indexPath]
根据
NSFetchedResultsChangeUpdate
事件时,此事件将崩溃,并出现
NSRangeException
异常


谢谢你的任何帮助或想法

我现在找到了解决问题的办法。在更新事件的情况下,不需要调用

[self.controller objectAtIndexPath:indexPath]
因为更新的对象已作为anObject参数提供给
-controller:didChangedObject:…
委托

因此,我将
-configureCell:atIndexPath:
替换为直接使用更新对象的
-configureCell:withObject:
方法。这似乎没有问题

代码现在如下所示:

- (void)configureCell:(UITableViewCell *)cell withObject:(MyManagedObject *)myObj
{
    cell.textLabel.text = myObj.name;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCellIdentifier"];
    [self configureCell:cell withObject:[self.controller objectAtIndexPath:indexPath]];
    return cell;
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;

    switch(type) {
    /* ... */           
    case NSFetchedResultsChangeUpdate:
        [self configureCell:[tableView cellForRowAtIndexPath:indexPath] withObject:anObject];
        break;
    /* ... */           
    }
}

这实际上非常常见,因为在启用核心数据的情况下创建新的主/详细项目时,会出现以下情况:

- (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:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath]
                  atIndexPath:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}
解决方案1:使用
anObject
当对象已经提供给您时,为什么要查询获取的结果控制器并冒险使用不正确的索引路径?还有

只需将助手方法
configureCell:atIndexPath:
从采用索引路径更改为采用已修改的实际对象:

- (void)configureCell:(UITableViewCell *)cell withObject:(NSManagedObject *)object {
    cell.textLabel.text = [[object valueForKey:@"timeStamp"] description];
}
在行的单元格中,使用:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    [self configureCell:cell withObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
    return cell;
}
最后,在更新中使用:

case NSFetchedResultsChangeUpdate:
    [self configureCell:[tableView cellForRowAtIndexPath:indexPath]
             withObject:anObject];
    break;
解决方案2:使用
newIndexPath
从iOS 7.1开始,当发生NSFetchedResultsChangeUpdate时,
indexPath
newIndexPath
都会传入

调用CellForRowatineXpath时,只需保留默认实现对indexPath的使用,但更改发送到newIndexPath的第二个索引路径:

case NSFetchedResultsChangeUpdate:
    [self configureCell:[tableView cellForRowAtIndexPath:indexPath]
            atIndexPath:newIndexPath];
    break;
解决方案#3:在索引路径处重新加载行 是重新加载索引路径。将配置单元格的调用替换为重新加载行的调用:

case NSFetchedResultsChangeUpdate:
    [tableView reloadRowsAtIndexPaths:@[indexPath]
                     withRowAnimation:UITableViewRowAnimationAutomatic];
    break;
这种方法有两个缺点:

  • 通过调用重载行,它将调用CyrFROWROW,这又调用DeQueReReUababeleLee标识符,它将重用现有的单元格,可能是“强”,去掉重要的状态< /强>(例如,如果单元格处于拖动La的中间)。
  • 它将错误地尝试重新加载不可见的单元格。在Apple的原始代码中,如果单元格不可见或
    indepath
    超出范围,则将返回“
    nil
    ”。因此,在调用重载行之前进行检查更为正确

  • 复制错误
  • 使用Xcode 6.4中的核心数据创建一个新的主/详细项目
  • 向核心数据
    事件
    对象添加标题属性
  • 使用多条记录填充表格(例如,在
    viewDidLoad
    中运行此代码)

  • 除了在点击“新建”按钮时添加记录外,还要更新最后一项(注意:这可以在创建项之前或之后完成,但请确保在调用“保存”之前完成!):

  • 运行应用程序。你应该看到五个项目

  • 点击新按钮。您将看到一个新项目被添加到顶部,并且最后一个项目没有文本“updated”,即使它本应该有文本“updated”。如果强制重新加载单元格(例如,通过将单元格从屏幕上滚动),它将显示文本“更新”
  • 现在实现上述三种解决方案之一,除了添加一个项目外,最后一个项目的文本将更改为“更新”

  • Ole Begemann有一篇关于这个问题的博客文章:(FWIW,我更喜欢你的解决方案,而不是他的。)@KristopherJohnson:谢谢你的反馈和链接(我只是偶尔遇到这个问题,而这篇博客文章包含了为什么会出现这个问题的好信息)是的,重新加载表行会导致重新绘制单元格,这有时是不需要的。可能的改进:如果
    [tableView CellForRowatineXpath:indexPath]
    返回nil,则该行不在屏幕上,并且无需调用
    configureCell:withObject:
    CellForRowatineXpath:
    无法返回nil。这将产生不一致性!如果调用了
    cellforrowatinexpath:
    ,这是因为
    numberOfRowsInSection:
    正在返回>0@CarlosRicardo:如果单元格不可见或indexPath超出范围,UITableView方法CellForRowatineXpath:将返回nil。-您可能指的是UITableViewDataSource方法tableView:CellForRowatineXpath:它必须返回有效的单元格,而不是nil。但是这里没有使用这个方法。是的,这就是我所说的方法,有没有其他的方法和我不知道的方法有相同的签名?因为我在上面看到了:
    tableView cellforrowatinexpath:indexath]
    。我想我错过了一些东西
    NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
    NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
    for (NSInteger i = 0; i < 5; i++) {
        NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
        [newManagedObject setValue:[NSDate date] forKey:@"timeStamp"];
        [newManagedObject setValue:[@(i) stringValue] forKey:@"title"];
    }
    [context save:nil];
    
    - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
        NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
        cell.textLabel.text = [NSString stringWithFormat:@"%@ - %@", [object valueForKey:@"timeStamp"], [object valueForKey:@"title"]];
    }
    
    // update the last item
    NSArray *objects = [self.fetchedResultsController fetchedObjects];
    NSManagedObject *lastObject = [objects lastObject];
    [lastObject setValue:@"updated" forKey:@"title"];