我有UITableView
使用NSFetchedResultsController
作为数据源。
核心数据存储在多个并行运行的后台线程中更新(每个线程使用它自己的NSManagedObjectContext
)。
主线程观察NSManagedObjectContextDidSaveNotification
通知并使用mergeChangesFromContextDidSaveNotification:
更新上下文。
有时发生NSFetchedResultsController
发送
具有不存在的indexPath的NSFetchedResultsChangeUpdate
事件
在那一点上。
例如:获取的结果控制器的结果集包含 1节有4个物体。第一个对象在一个线程中删除。 最后一个对象在另一个线程中更新。然后有时候 发生以下情况:
但是获取的结果控制器现在只包含3个对象,如果调用
MyManagedObject *obj = [controller objectAtIndexPath:indexPath]
根据NSFetchedResultsChangeUpdate
更新表格视图单元格
事件,此事件因NSRangeException
例外而崩溃。
感谢您提供任何帮助或想法!
答案 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;
}
}
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;
newIndexPath
从iOS 7.1开始,当发生NSFetchedResultsChangeUpdate时,indexPath
和newIndexPath
都会被传入。
在调用cellForRowAtIndexPath时,只需保留indexPath的默认实现用法,但更改发送到newIndexPath的第二个索引路径:
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:newIndexPath];
break;
Ole Begemann's solution是重新加载索引路径。用调用重新加载行替换调用配置单元格:
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
此方法有两个缺点:
nil
超出范围,cellForRowAtIndexPath:将返回" indexPath
。"因此,在调用重载行之前检查indexPathsForVisibleRows会更正确。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"]];
}
除了在点击新按钮时添加记录,还要更新最后一项(注意:这可以在项目创建之前或之后完成,但请确保在调用保存之前执行此操作!) :
// update the last item
NSArray *objects = [self.fetchedResultsController fetchedObjects];
NSManagedObject *lastObject = [objects lastObject];
[lastObject setValue:@"updated" forKey:@"title"];
运行该应用。你应该看到五个项目。