NSFetchedResultsControllerとUITableViewでハマる

2010/09/16

CoreDataの更新処理でかなりハマりました(>_<)

NSFetchedResultsControllerを利用してCoreDataの更新やフェッチ処理をしていますが、 ソートをかけた状態でデータ更新処理がうまくいかない事があります。

iphone 上記の項目名を変更する際、並び順が変わるような更新が発生した時にエラーになりました。

エラー内容

Assertion failure in -[UITableView _endCellAnimationsWithContext:]

エラーからするとUITableViewのnumberOfSectionsInTableView、numberOfRowsInSection等で更新処理が正常にいっていないようです。

原因

更新後にUITableViewのメソッドがfetchedResultsControllerを読んだ時点で、ManagedObjectContextの不整合が起きているものと思われます。

Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (5) must be equal to the number of rows contained in that section before the update (5), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted). with userInfo (null)

更にエラーを読み解いてくと、NSManagedObjectContextObjectsDidChangeNotificationが発生した時に、UITableViewのsectionが0になってしまいます。 また更新前と更新後のUITableViewのrowsの数も異なっているようです。

参考ページ

で、同じようにハマっている人がいました。 ・Study CoreData 19 ~最悪のシナリオ~-[NSFetchedResultsController performFetch:] でクラッシュ

対策

うーん、ちょっと大変そう。 ログを追跡してわかりましたが、NSManagedObjectContextをsaveする以前に、NSManagedObjectのデータを変更し、ソートが発生するような一覧になった時にこの現象が起きるようです。 NSFetchedResultsControllerとUITableViewのメソッドで何が行われているか?もう少し理解が必要かも。

ちなみに、UITableViewをNSManagedObjectContextで管理せず、UITableView用のNSArrayで管理すれば問題は回避できそうだけど、何だか本末転倒な気がする。 NSManagedとNSArrayでわけて管理するとメモリの無駄使いや、混乱して違うバグを生む可能性も否めない。

とりあえず、didChangeObjectの処理に問題がありそうなので、そこから手をつけていくことにした。

- (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;
    }
}

並び替えが発生するデーター更新の場合、NSFetchedResultsChangeMoveを経由することになり、どうやらdeleteRowsAtIndexPaths、InsertRowsAtIndexPathsの処理で不整合になるようです。

応急処置

        case NSFetchedResultsChangeMove:
           [tableView reloadData]
           break;

とりあえず、行を削除せずにUITableViewのデータをリロードする事で回避できましたが、今度はUITableViewのdidSelectRowAtIndexPathの処理で整合性が・・・

そこで、fetchedResultsControllerメソッドの雛形の部分で、fetchedResultsController_をキャッシュしているような箇所があったので、コメントアウトしてみた。

    if (fetchedResultsController_ != nil) {
        //return fetchedResultsController_;
    }

これで、didSelectRowAtIndexPathクリック後の処理もうまくいったが、毎回NSFetchRequestを発行する事になります・・・。

----2010/09/01追記 「return fetchedResultsController_」のコメントアウトは、しなくても良かったです。 (当然と言えば当然か・・・) fetchedResultsControllerを複数のクラスで記述したり、共有したりしていた自分のバグでした。

今回の例はセクションを利用していないので、セクションを利用する場合はUITableViewとの連携がもっと大変です。 肝は、

fetchedResultsControllerのソート順とUITableViewのソート順が常に一致している

ことです。