4

私は 2 つのマネージド オブジェクト コンテキストを持っています。(1) NSMainQueueConcurrencyTypeUI/メイン スレッドで使用されるものとして作成され、(2)NSPrivateQueueConcurrencyTypeネットワークで使用されるものとして作成されます。これらのコンテキストはどちらも永続ストアに移動します (つまり、親/子コンテキストは使用していません)。

ビュー コントローラーについては、最初の UI マネージド オブジェクト コンテキストを使用する を使用していますUITableViewControllerNSFetchedResultsController

NSManagedObjectContextDidSaveNotification. _

アプリは、2 番目のコンテキストで新しいオブジェクトが挿入され、既存のオブジェクトが削除される原因となるネットワーク応答を処理するまで正常に動作します。2 番目のコンテキストが保存されると、NSManagedObjectContextDidSaveNotification発火と変更が 1 番目のコンテキストにマージされます。のデリゲート メソッド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

NSFetchedResultsControllerNSFetchRequestに応答してセルを準備するときにを使用すると、非常に混乱しますtableView:cellForRowAtIndexPath:。を実行しなければNSFetchRequest、すべて問題ありません。NSFetchedResultsControllerただし、そうすると、さらに変更通知を実行するようにトリガーされ、 UITableView.

includesPendingChanges = NOこれを回避するには、NSFetchRequestを設定します。

14048101詳細な例とサンプルアプリを使用して、これに関するレーダー問題 (問題 ID) を開きました。このバグは、iOS 5.1、6.0、および 6.1 で再現されます。

サンプル アプリでは、Xcode の CoreData テンプレートにログを追加して、NSFetchedResultsControllerデリゲート メソッドの開始/終了をログに記録しました。ネットワーク コンテキストでオブジェクトを挿入 + 削除すると、ログに次のように表示されます。

01: => (前) mergeChangesFromContextDidSaveNotification 02: => (入力) controllerWillChangeContent カウント=4 03: <= (離れる) controllerWillChangeContent カウント=4 04: didChangeObject タイプ=1 indexPath=(null) newIndexPath= 2 インデックス [0, 0] 05: => (入力) controllerDidChangeContent count=5

この時点で、すべてが良好です。controllerDidChangeContent:は、1 つの挿入を処理するために呼び出されまし[tableView endUpdates]た。tableView:cellForRowAtIndexPath:configureCell:atIndexPath:

06: => (入力) 行 0 のセルを構成

この時点で、configureCell:atIndexPath:を作成しNSFetchRequestて呼び出します[self.managedObjectContext executeFetchRequest:error:]。ここから問題が始まります。このフェッチ要求を実行すると、挿入の処理が完了する前に、コンテキスト内の残りの変更 (1 つの削除と 3 つの更新) の処理がトリガーされます (controllerDidChangeContent:行番号 05 で入力し、行番号 16 まで終了しません)。

07: => (入力) controllerWillChangeContent カウント=5 08: <= (終了) controllerWillChangeContent カウント=5 09: didChangeObject タイプ=2 indexPath= 2 インデックス [0, 4] newIndexPath=(null) 10: didChangeObject タイプ=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=( null) 13: => (入力) controllerDidChangeContent count=4

この時点で、フレームワークは への再入可能な呼び出しを行っていcontrollerDidChangeContent:ます。

14: <= (去る) controllerDidChangeContent count=4 15: <= (去る) 行 0 でセルを構成する 16: <= (去る) controllerDidChangeContent count=4 17: <= (後) mergeChangesFromContextDidSaveNotification

この時点で、(1) 新しいセルが追加されたこと、(2) 3 つのセルが更新されたこと、(3) 削除されたセルがまだ表示されていることを UI で確認できますが、これは誤りです。

UI でさらに操作を行った後、通常、Assertion failureまたはメッセージが無効なオブジェクトの例外に送信されます。

サンプル アプリはhttps://github.com/peymano/CoreDataFetchedResultsControllerで入手できます。

于 2013-06-03T21:13:27.180 に答える