这实际上很常见,因为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 发生时,两者都会indexPath
被newIndexPath
传入。
只需在调用 cellForRowAtIndexPath 时保持默认实现对 indexPath 的使用,但将发送到 newIndexPath 的第二个索引路径更改为:
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:newIndexPath];
break;
解决方案#3:在索引路径重新加载行
Ole Begemann 的解决方案是重新加载索引路径。将配置单元格的调用替换为重新加载行的调用:
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
这种方法有两个缺点:
- 通过调用重新加载行,它会调用 cellForRow,而后者又会调用 dequeueReusableCellWithIdentifier,这将重用现有的单元格,可能会摆脱重要状态(例如,如果单元格正处于被拉邮箱样式的中间)。
- 它会错误地尝试重新加载不可见的单元格。在 Apple 的原始代码中,cellForRowAtIndexPath:将返回“
nil
如果单元格不可见或indexPath
超出范围”。因此,在调用重新加载行之前检查indexPathsForVisibleRows会更正确。
重现错误
- 在 Xcode 6.4 中使用核心数据创建一个新的主/详细项目。
- 向核心数据
event
对象添加标题属性。
用几条记录填充表(例如在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];
更改配置单元格以显示标题属性:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:@"%@ - %@", [object valueForKey:@"timeStamp"], [object valueForKey:@"title"]];
}
除了在点击新按钮时添加一条记录,更新最后一个项目(注意:这可以在项目创建之前或之后完成,但请确保在调用 save 之前完成!):
// update the last item
NSArray *objects = [self.fetchedResultsController fetchedObjects];
NSManagedObject *lastObject = [objects lastObject];
[lastObject setValue:@"updated" forKey:@"title"];
运行应用程序。您应该看到五个项目。
- 点击新按钮。您将看到一个新项目被添加到顶部,并且最后一个项目没有文本“更新”,即使它应该有它。如果您强制重新加载单元格(例如,通过将单元格滚动到屏幕外),它将显示文本“已更新”。
- 现在实施上述三个解决方案之一,除了添加一个项目外,最后一个项目的文本将更改为“已更新”。