12

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

核心数据存储在并行运行的多个后台线程中更新(每个线程使用它自己的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: 使用 type = NSFetchedResultsChangeUpdate, indexPath.row = 3 调用。

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

MyManagedObject *obj = [controller objectAtIndexPath:indexPath]

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

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

4

2 回答 2

16

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

[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;
    /* ... */           
    }
}
于 2012-07-16T11:28:59.667 回答
9

这实际上很常见,因为Apple 的 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也推荐此解决方案。

只需将辅助方法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 的解决方案是重新加载索引路径。将配置单元格的调用替换为重新加载行的调用:

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

这种方法有两个缺点:

  1. 通过调用重新加载行,它会调用 cellForRow,而后者又会调用 dequeueReusableCellWithIdentifier,这将重用现有的单元格,可能会摆脱重要状态(例如,如果单元格正处于被拉邮箱样式的中间)。
  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. 现在实施上述三个解决方案之一,除了添加一个项目外,最后一个项目的文本将更改为“已更新”。
于 2015-08-25T20:04:35.747 回答