4

我有 2 个托管对象上下文:(1)创建为NSMainQueueConcurrencyTypeUI/主线程使用,(2)创建为NSPrivateQueueConcurrencyType网络使用。这两个上下文都进入持久存储(即,我没有使用父/子上下文)。

对于视图控制器,我使用了UITableViewController一个NSFetchedResultsController使用第一个 UI 托管对象上下文的 a。

我通过观察将第二个托管对象上下文中的更改合并到第一个上下文中NSManagedObjectContextDidSaveNotification

该应用程序工作正常,直到它处理导致在第二个上下文中插入新对象删除现有对象的网络响应。保存第二个上下文后,NSManagedObjectContextDidSaveNotification触发和更改将合并到第一个上下文中。的委托方法NSFetchedResultsController被调用并向表中添加了一个新行,但表示已删除对象的行没有被删除。

如果我尝试对表视图执行其他操作,例如重新加载表或更新其他对象,我会在控制台日志中得到以下断言:

*** Assertion failure in -[UITableView _endCellAnimationsWithContext:],
/SourceCache/UIKit/UIKit-2380.17/UITableView.m:1070

CoreData: error: Serious application error.  An exception was caught
from the delegate of NSFetchedResultsController during a call to -
controllerDidChangeContent:.  Invalid update: invalid number of rows in
section 0.  The number of rows contained in an existing section after the
update (6) must be equal to the number of rows contained in that section
before the update (7), plus or minus the number of rows inserted or deleted
from that section (6 inserted, 6 deleted) and plus or minus the number of rows
moved into or out of that section (0 moved in, 0 moved out). with 
userInfo (null)

通常,如果您在使用UITableView的批量更新方法时忘记更新模型对象,则会收到此错误,但在这种情况下,NSFetchedResultsController正在完成所有工作。我的委托方法是样板:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
  [self.tableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
  switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
  NSLog(@"    didChangeObject type=%d indexPath=%@ newIndexPath=%@", type, indexPath, newIndexPath);

  UITableView *tableView = self.tableView;

    switch(type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

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

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
  [self.tableView endUpdates];
}

我的 UITableViewDataSourcetableView:cellForRowAtIndexPath方法是:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  static NSString *CellIdentifier = @"Cell";

  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  if (cell == nil) {
      cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
      cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
  }

  [self configureCell:cell atIndexPath:indexPath];

  return cell;
}

并且configureCell:是:

- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
  Event *event = (Event *)[self.fetchedResultsController objectAtIndexPath:indexPath];
  cell.textLabel.text = [[event valueForKey:@"timeStamp"] description];

  NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Owner"];
  NSArray *objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];
  cell.detailTextLabel.text = [objects.lastObject name]; // simplified
}
4

1 回答 1

10

NSFetchedResultsController如果您NSFetchRequest在准备单元格以响应tableView:cellForRowAtIndexPath:. 如果你不执行 a NSFetchRequest,一切都很好。但是,如果您这样做,它会触发NSFetchedResultsController执行进一步的更改通知,这对UITableView.

解决方法是includesPendingChanges = NO在您的 NSFetchRequest 上进行设置。

14048101我已经通过详细的示例和示例应用程序打开了一个关于此问题的雷达问题 - 问题 ID 。此错误在 iOS 5.1、6.0 和 6.1 上重现。

NSFetchedResultsController在我的示例应用程序中,我将日志记录添加到 Xcode 的 CoreData 模板以记录委托方法的进入/离开。当我在网络上下文中插入 + 删除对象时,日志显示:

01:=>(之前)mergeChangesFromContextDidSaveNotification 02:=>(输入)controllerWillChangeContent 计数=4 03:<=(离开)controllerWillChangeContent 计数=4 04:didChangeObject 类型=1 indexPath=(null)newIndexPath=2 个索引[0, 0] 05:=>(输入)controllerDidChangeContent 计数=5

在这一点上,一切都很好。controllerDidChangeContent:已被调用来处理 1 个插入,其中调用[tableView endUpdates],调用tableView:cellForRowAtIndexPath:,调用configureCell:atIndexPath:

06: => (enter) 配置第 0 行的单元格

在这一点上,configureCell:atIndexPath:创建一个NSFetchRequest并调用[self.managedObjectContext executeFetchRequest:error:]——这里开始坏事。执行此获取请求会在插入处理完成之前触发上下文中剩余更改的处理(1 次删除和 3 次更新)(我们controllerDidChangeContent:在第 05 行输入,直到第 16 行才离开)。

07: => (enter) controllerWillChangeContent count=5 08: <= (leave) controllerWillChangeContent count=5 09: didChangeObject type=2 indexPath= 2 个索引 [0, 4] newIndexPath=(null) 10: didChangeObject type=4 indexPath= 2 个索引 [0, 2] newIndexPath=(null) 11: didChangeObject type=4 indexPath= 2 个索引 [0, 1] newIndexPath=(null) 12: didChangeObject type=4 indexPath= 2 个索引 [0, 3] newIndexPath=(空)13:=>(输入)controllerDidChangeContent count=4

此时,框架正在对controllerDidChangeContent:.

14: <= (leave) controllerDidChangeContent count=4 15: <= (leave) 在第 0 行配置单元格 16: <= (leave) controllerDidChangeContent count=4 17: <= (after) mergeChangesFromContextDidSaveNotification

此时,您可以在 UI 中看到:(1) 添加了一个新单元格,(2) 更新了 3 个单元格,以及 (3) 删除的单元格仍然可见,这是错误的。

在 UI 中进行进一步操作后,我通常会收到一条Assertion failure或消息发送到无效对象异常。

我的示例应用程序位于https://github.com/peymano/CoreDataFetchedResultsController

于 2013-06-03T21:13:27.180 回答