Ошибка CoreData сводит меня с ума... CoreData: серьезная ошибка приложения. Исключение, вызванное делегатом NSFetchedResultsController

Мое приложение имеет две панели вкладок... Каждый из них берет пользователя в tableviewcontroller, который представляет ему список элементов. Первое представление позволяет пользователю записывать записи в базу данных. Другая вкладка/представление читается из базы данных и также представляет эти элементы пользователю, однако никаких обновлений в хранилище coreData/persistant из этого второго представления не производится.

Когда я добавляю новый элемент через первый диспетчер представлений, он отлично отображается в представлении. Однако, как только я нажимаю на другую панель вкладок, чтобы увидеть, что новый элемент появляется в этом диспетчере просмотра, я получаю указанную ниже ошибку, а вновь добавленный элемент не отображается... Примечание: если я остановлю приложение и перезагрузим/перезапустите его и начните, нажав вторую панель вкладок, новый элемент отобразится отлично, поэтому я знаю, что модель обновляется отлично.

*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1912.3/UITableView.m:1046
2011-10-20 20:56:15.117 Gtrac[72773:fb03] 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 (4) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 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)

Код из приложения-делегата, где управляемый объектObjectContext передается двум диспетчеру просмотра.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {


    // get a point to the master database context
    NSManagedObjectContext *context = [self managedObjectContext];
    if (!context) {
        // Handle the error.
    }

    // create tab bar controller and array to hold each of the tab-based view controllers that will appear as icons at bottom of screen
    tabBarController = [[UITabBarController alloc] init];
    NSMutableArray *localControllersArray = [[NSMutableArray alloc] initWithCapacity:5];    


    //
    // setup first tab bar item
    //
    //
    // alloc the main view controller - the one that will be the first one shown in the navigation control
    RootViewController *rootViewController = [[RootViewController alloc] initWithTabBar];

    // Pass the managed object context to the view controller.
    rootViewController.managedObjectContext = context;

    // create the navigation control and stuff the rootcontroller inside it
    UINavigationController *aNavigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController];

    // set the master navigation control 
    self.navigationController = aNavigationController;

    // add the navigaton controller as the first tab for the tab bar
    [localControllersArray addObject:aNavigationController];

    [rootViewController release];
    [aNavigationController release];    


    //
    // setup the other tab bar
    //
    //

    // alloc the view controller 
    vcSimulator *vcSimulatorController = [[vcSimulator alloc] initWithTabBar];

    UINavigationController *blocalNavigationController = [[UINavigationController alloc] initWithRootViewController:vcSimulatorController];

    // Pass the managed object context to the view controller.
    vcSimulatorController.managedObjectContext = context;

    // add this controller to the array of controllers we are building
    [localControllersArray addObject:blocalNavigationController];

    // release these guys, they are safely stored in the array - kill these extra references
    [blocalNavigationController release];
    [vcSimulatorController release];


    //
    //
    // ok, all the tab bars are in the array - get crackin
    //
    //
    // load up our tab bar controller with the view controllers
    tabBarController.viewControllers = localControllersArray;

    // release the array because the tab bar controller now has it
    [localControllersArray release];

    [window addSubview:[tabBarController view]];
    [window makeKeyAndVisible];

    return YES;




When I add a new item via the first viewcontroller, it shows up perfectly in the view.  However, as soon as I tap on the other tab bar to see the new item appear in that viewcontroller, I get the error listed above, and the newly added item does not appear...   Note: if I stop the app and reload/re-run it, and start by tapping the 2nd tabbar, the new item shows up fine, so I know the model is being updated fine.

Here are the tableview delegate methods from the 2nd view controller.



- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {

    [self.tableView beginUpdates];
}


- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {

    NSLog(@">>> Entering %s [Line %d] ", __PRETTY_FUNCTION__, __LINE__);    
    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;
    }

}


- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
    NSLog(@">>> Entering %s [Line %d] ", __PRETTY_FUNCTION__, __LINE__);    

    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)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];

}

Любая помощь, которую вы можете предоставить, будет очень оценена.

Я искал этот сайт и нашел много примеров этой ошибки, но ни один из них не подходит. Я также видел ссылки на то, что эта ошибка, которую я вижу, на самом деле является известной ошибкой в ​​коде Apple...

* ОБНОВЛЕННАЯ ИНФОРМАЦИЯ *

Я вернулся и установил точки останова в коде и редактирую исходный вопрос с помощью этой дополнительной информации. Когда пользователь добавляет новый элемент в базу данных, он переходит из корневого представления в представление listCourses. Операция добавления работает безупречно, и listCourses View UITableView отлично обновляется.

Когда я нажимаю на другое представление, которое также считывает данные из одной и той же модели данных ядра, он контролирует просмотр в следующей последовательности, но никогда не заканчивает добавление нового элемента в представление таблицы. Вот последовательность, через которую она проходит.

Симулятор VC:

- controllerWillChangeContent which runs...
        [self.tableView beginUpdates];

    - didChangeObject 
        ..with message: NSFetchedResultsChangeUpdate
        ..which ran:
        [self configureCell:[tableView cellForRowAtIndexPath:indexPath] 

    - controllerDidChangeContent:   
        [self.tableView endUpdates];

Другой наблюдающий контроллер, который отлично работает, проходит через эту последовательность сразу после добавления записи в базу данных.

ListCourses VC:

- didChangeSection
    ...with message:  NSFetchedResultsChangeInsert
...which ran:
    [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];

- didChangeObject
    ..with message: NSFetchedResultsChangeInsert
    ..which ran:
    [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];

Почему один диспетчер представлений получает сообщение NSFetchedResultsChangeInsert, а другое нет?

Вот методы делегата из неисправного диспетчера представлений.

// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // Delete the row from the data source
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
    }   
    else if (editingStyle == UITableViewCellEditingStyleInsert) {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
    }   
}





- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller is about to start sending change notifications, so prepare the table view for updates.
    NSLog(@">>> Entering %s [Line %d] ", __PRETTY_FUNCTION__, __LINE__);    

    [self.tableView beginUpdates];
}


- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
    NSLog(@">>> Entering %s [Line %d] ", __PRETTY_FUNCTION__, __LINE__);    


    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:

            //[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath] 
                    atIndexPath:indexPath];
            //[tableView reloadData];


            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];

            // Reloading the section inserts a new row and ensures that titles are updated appropriately.
            // [tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];

            break;
    }
    NSLog(@"vc>>>  about to reload data");
    //  [self.tableView reloadData];

}


- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
    NSLog(@">>> Entering %s [Line %d] ", __PRETTY_FUNCTION__, __LINE__);    

    switch(type) {

        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

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

}


- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller has sent all current change notifications, so tell the table view to process all updates.
    NSLog(@">>> Entering %s [Line %d] ", __PRETTY_FUNCTION__, __LINE__);    
    [self.tableView endUpdates];

}

Спасибо, фил

Ответ 1

Проверка работоспособности UITableView работает следующим образом:

В строке [self.tableView beginUpdates]; tableView вызывает ваш метод tableView: numberOfRowsInSection: delegate, который, как представляется, возвращается 3. В строке [self.tableView endUpdates]; он вызывает его снова и, кажется, возвращается 4. Поэтому tableView ожидает, что вы вставляете 1 строку между этими двумя строками. Фактически, никакие строки не вставлены, поэтому tableView не выполняет утверждение. (Вы можете увидеть ожидаемое и фактическое количество строк в сообщении подтверждения).

Увеличение от 3 строк до 4 строк показывает, что ваш NSFetchedResultsController замечает вновь вставленный элемент Core Data правильно. То, что вам нужно сделать, это поставить точку останова в начале вашего контроллера: didChangeObject: atIndexPath: forChangeType: метод и пройти через него, когда вы переключаетесь на вторую вкладку после вставки элемента. Вы должны увидеть NSFetchedResultsChangeInsert: случай выполнения оператора switch, но это, очевидно, не происходит.

Надеюсь, вы можете понять, почему вставка не происходит - в противном случае вернитесь и сообщите нам, что вы на самом деле видели при переходе через этот метод.

ИЗМЕНИТЬ ДОБАВИТЬ:

ОК, поэтому методы делегирования NSFetchedResultsController во втором контроллере представления вызывается при переключении на эту вкладку, а не сразу, когда новый элемент вставлен на вкладку 1. Это означает, что второй контроллер представления не видит вставку (которая должен произойти немедленно) и на самом деле отвечает на некоторые другие уведомления об обновлении Core Data позже, которые возникают при переключении на вкладку 2. Контроллер получаемых результатов работает со устаревшей информацией в строке beginUpdates (на самом деле в наборе результатов на самом деле нет 4 элементов 3). К тому времени, когда он попадает на endUpdates, он обновил свою выборку и обнаружил неожиданную вставку.

Методы делегирования NSFetchedResultsController действительно предназначены для обновления интерфейса на месте, пока вы вносите изменения, и вид контроллера отображается. В вашем случае вы вносите изменения, а THEN отображает новый контроллер вида. Шаблон, который вы действительно должны использовать, - это обновить представление таблицы в вашем методе viewWillAppear контроллера 2. Что-то вроде этого должно сделать это:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    NSError *error = nil;
    [resultsController performFetch:&error]; // Refetch data
    if (error != nil) {
        // handle error
    }

    [self.tableView reloadData];
}

Это гарантирует, что всякий раз, когда вы переключаетесь на вкладку 2, она работает со свежими данными из модели.