NSFetchedResultsController的NSFetchedResultsChangeUpdate事件中的NSRangeException异常

时间:2012-07-11 12:21:40

标签: core-data nsfetchedresultscontroller

我有UITableView使用NSFetchedResultsController作为数据源。

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

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

有时发生NSFetchedResultsController发送 具有不存在的indexPath的NSFetchedResultsChangeUpdate事件 在那一点上。

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

  • controllerWillChangeContent:被调用。
  • controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:with with with type = NSFetchedResultsChangeDelete,indexPath.row = 0.
  • controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:with with with type = NSFetchedResultsChangeUpdate,indexPath.row = 3.

但是获取的结果控制器现在只包含3个对象,如果调用

MyManagedObject *obj = [controller objectAtIndexPath:indexPath]

根据NSFetchedResultsChangeUpdate更新表格视图单元格 事件,此事件因NSRangeException例外而崩溃。

感谢您提供任何帮助或想法!

2 个答案:

答案 0 :(得分:14)

我现在找到了解决问题的方法。如果是更新事件,则无需调用

[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;
    /* ... */           
    }
}

答案 1 :(得分:6)

这实际上很常见,因为bug in Apple's boiler plate code for NSFetchedResultsControllerDelegate是您在创建启用了Core Data的新主/明细项目时获得的:

- (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

为什么在已经给出对象时使用不正确的索引路径查询获取的结果控制器和风险? Martin R recommends this solution也是如此。

只需将辅助方法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时,indexPathnewIndexPath都会被传入。

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

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

解决方案#3:在索引路径

重新加载行

Ole Begemann's solution是重新加载索引路径。用调用重新加载行替换调用配置单元格:

case NSFetchedResultsChangeUpdate:
    [tableView reloadRowsAtIndexPaths:@[indexPath]
                     withRowAnimation:UITableViewRowAnimationAutomatic];
    break;

此方法有两个缺点:

  1. 通过调用重载行,它将调用cellForRow,后者又调用dequeueReusableCellWithIdentifier,它将重用现有的单元格,可能摆脱重要状态(例如,如果单元格处于中间位置)拖了一个la Mailbox style)。
  2. 错误地尝试重新加载不可见的单元格。在Apple的原始代码中,如果单元格不可见或nil超出范围,cellForRowAtIndexPath:将返回" indexPath。"因此,在调用重载行之前检查indexPathsForVisibleRows会更正确。
  3. 重现错误

    1. 使用Xcode 6.4中的核心数据创建一个新的主/明细项目。
    2. 将标题属性添加到核心数据event对象。
    3. 使用多个记录填充表格(例如,在viewDidLoad运行此代码中)

      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];
      
    4. 更改配置单元格以显示标题属性:

      - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
          NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
          cell.textLabel.text = [NSString stringWithFormat:@"%@ - %@", [object valueForKey:@"timeStamp"], [object valueForKey:@"title"]];
      }
      
    5. 除了在点击新按钮时添加记录,还要更新最后一项(注意:这可以在项目创建之前或之后完成,但请确保在调用保存之前执行此操作!) :

      // update the last item
      NSArray *objects = [self.fetchedResultsController fetchedObjects];
      NSManagedObject *lastObject = [objects lastObject];
      [lastObject setValue:@"updated" forKey:@"title"];
      
    6. 运行该应用。你应该看到五个项目。

    7. 点按新按钮。你会看到一个新项目被添加到顶部,而最后一个项目没有文本&#34;更新,&#34;即使应该有它。如果您强制重新加载单元格(例如,通过滚动屏幕上的单元格),它将更新文本&#34;&#34;
    8. 现在实施上述三种解决方案中的一种,除了要添加的项目之外,最后一项的文本将更改为&#34;已更新。&#34;