【问题标题】:NSRangeException exception in NSFetchedResultsChangeUpdate event of NSFetchedResultsControllerNSFetchedResultsController 的 NSFetchedResultsChangeUpdate 事件中的 NSRangeException 异常
【发布时间】:2012-07-11 01:34:03
【问题描述】:

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

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

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

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

例如:fetched results 控制器的结果集包含 1 部分有 4 个对象。第一个对象在一个线程中被删除。 最后一个对象在不同的​​线程中更新。然后有时 发生以下情况:

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

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

MyManagedObject *obj = [controller objectAtIndexPath:indexPath]

根据NSFetchedResultsChangeUpdate更新表格视图单元格 事件,这会因NSRangeException 异常而崩溃。

感谢您的任何帮助或想法!

【问题讨论】:

  • Ole Begemann 有一篇关于此问题的博客文章:oleb.net/blog/2013/02/…(FWIW,我更喜欢你的解决方案而不是他的解决方案。)
  • @KristopherJohnson:感谢您的反馈和链接(我只是偶尔遇到问题,并且该博客条目包含很好的信息为什么会出现问题)。 - 是的,重新加载表格行会导致单元格被重绘,这有时是不希望的。

标签: core-data nsfetchedresultscontroller


【解决方案1】:

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

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

【讨论】:

  • 可能的改进:如果[tableView cellForRowAtIndexPath:indexPath]返回nil,则该行不在屏幕上,无需调用configureCell:withObject:
  • cellForRowAtIndexPath: 不能返回 nil。就会产生矛盾!如果cellForRowAtIndexPath: 被调用,那是因为numberOfRowsInSection: 正在返回> 0
  • @CarlosRicardo:UITableView 方法 cellForRowAtIndexPath:如果单元格不可见或 indexPath 超出范围,则返回 nil。 - 你可能的意思是 UITableViewDataSource 方法 tableView:cellForRowAtIndexPath: 它必须返回一个有效的单元格而不是 nil。但是这里没有使用这种方法。
  • 是的,这就是我所说的方法,还有其他我不知道的具有相同签名的方法吗?因为我在上面看到:tableView cellForRowAtIndexPath:indexPath]。我想我在这里遗漏了一些东西
  • @CarlosRicardo: [tableView cellForRowAtIndexPath:indexPath] 调用 UITableView 的 cellForRowAtIndexPath: 方法。 - 您将它与 UITableViewDataSource 委托协议的 tableView:cellForRowAtIndexPath: 方法混淆了,该方法将被称为[dataSource tableView:tableView cellForRowAtIndexPath:indexPath]
【解决方案2】:

这实际上很常见,因为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. 通过调用 reload rows,它会调用 cellForRow,而 cellForRow 又会调用 dequeueReusableCellWithIdentifier,这将重用现有的单元格,可能摆脱重要状态(例如,如果单元格正在拖了拉Mailbox style)。
  2. 它会错误地尝试重新加载不可见的单元格。在 Apple 的原始代码中,cellForRowAtIndexPath: 将返回“nil 如果单元格不可见或indexPath 超出范围。”因此,在调用重新加载行之前检查 indexPathsForVisibleRows 会更正确。

重现错误

  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. 除了在点击新按钮时添加一条记录,更新最后一个项目(注意:这可以在项目创建之前或之后完成,但请确保在调用 save 之前完成!):

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-01-02
    • 1970-01-01
    • 2016-09-07
    • 2013-10-06
    • 2011-06-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多