这实际上很常见,因为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 发生时,indexPath 和 newIndexPath 都会被传入。
在调用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;
这种方法有两个缺点:
- 通过调用 reload rows,它会调用 cellForRow,而 cellForRow 又会调用 dequeueReusableCellWithIdentifier,这将重用现有的单元格,可能摆脱重要状态(例如,如果单元格正在拖了拉Mailbox style)。
- 它会错误地尝试重新加载不可见的单元格。在 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"];
运行应用程序。您应该会看到五个项目。
- 点击新按钮。您将看到一个新项目被添加到顶部,并且最后一个项目没有文本“更新”,即使它应该有它。如果您强制重新加载单元格(例如,通过将单元格滚动到屏幕外),它将显示文本“已更新”。
- 现在实施上述三个解决方案之一,除了添加一个项目外,最后一个项目的文本将更改为“已更新”。