Основные данные, поддерживаемые UITableView, выдают ошибку при удалении ячеек

У меня есть UITableView, поддерживаемый массивом объектов Core Data, созданных путем добавления двух массивов объектов Core Data с именами папок и заметок. Всякий раз, когда я пытаюсь удалить строку из представления таблицы, она выдает:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0.  The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'

Вот код для удаления (в моем подклассе UITableViewCell):

NSManagedObjectContext *context = [NSManagedObjectContext MR_contextForCurrentThread];
[object MR_deleteInContext:context];
[context MR_saveToPersistentStoreAndWait];
[tableViewController loadData];
[tableView deleteRowsAtIndexPaths:@[self.indexPath] withRowAnimation:UITableViewRowAnimationLeft];

Примечание: методы MR_ - это методы MagicalRecord, они делают то, что говорят.

Вот метод loadData в UITableViewController (в основном обновляет массив):

NSPredicate *textParentFilter = [NSPredicate predicateWithFormat:@"parent == %@", self.parent];
self.notes = [Note MR_findAllWithPredicate:textParentFilter];
NSPredicate *folderParentFilter = [NSPredicate predicateWithFormat:@"parentFolder == %@", self.parent];
self.folders = [Folder MR_findAllWithPredicate:folderParentFilter];

Также:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    // Return the number of rows in the section.
    return [self.notes count] + [self.folders count];
}

Я понимаю, почему получаю исключение: я удаляю строку в loadData, а затем numberOfRowsInSection не синхронизируется с tableview и Core Data store. Однако, если я заменю deleteRowsAtIndexPaths на [tableView reloadData], он удалится нормально (но без анимации). Я пробовал beginUpdates и endUpdates, меняя порядок удаления вещей, но безрезультатно.


person Januzellij    schedule 17.02.2014    source источник
comment
Ваша проблема в том, что self.notes.count + self.folders.count == 2, как до, так и после того, как вы сообщаете табличному представлению, что вы что-то удалили. Итак, либо вы фактически ничего не удаляли из своей модели данных, либо вы удалили, а затем что-то добавили.   -  person Aaron Brager    schedule 17.02.2014
comment
Кстати, в ответ на попытку нескольких заказов правильный порядок: (1) удалить элемент из источника данных, (2) вызвать deleteRowsAtIndexPaths:. Не звоните deleteRowsAtIndexPaths:, пока self.notes.count + self.folders.count не будет == 1.   -  person Aaron Brager    schedule 17.02.2014
comment
Тестирование на одном примечании: до того, как loadData NSArray *t = [tableController.folders arrayByAddingObjectsFromArray:tableController.notes]; имел один объект, после него был 0 (что, как я полагаю, является правильным). Выкидывает Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete row 0 from section 0 which only contains 0 rows before the update'   -  person Januzellij    schedule 17.02.2014
comment
@Januzellij, что делает [tableViewController loadData];?   -  person Akhilrajtr    schedule 17.02.2014
comment
Я объяснил это в своем вопросе   -  person Januzellij    schedule 17.02.2014
comment
Похоже, у вас может быть состояние гонки. Вся ваша работа по удалению выполняется в основном потоке?   -  person Aaron Brager    schedule 17.02.2014
comment
Насколько я могу судить, да.   -  person Januzellij    schedule 17.02.2014
comment
Я настоятельно призываю вас отказаться от MR_contextForCurrentThread. Со временем это приведет к случайным сбоям на основе потоков. Рекомендуемый шаблон аналогичен, но больше ручной. Создайте новый контекст для своей беседы и избавьтесь от него, когда закончите.   -  person casademora    schedule 17.02.2014


Ответы (3)


При работе с UITableViews и Core Data важно, чтобы объекты в вашем хранилище (например, массивы ваших заметок и папок) всегда соответствовали вызовам делегата / источника данных UITableView. Когда вы начинаете анимировать ячейки для добавления, удаления и обновления, вы сталкиваетесь с проблемами синхронизации, когда иногда значения не выравниваются. Хороший пример - это когда вам нужно последовательно выполнять несколько добавлений / удалений ячеек при отображении или открытии раздела таблицы. Если вы используете [tableView reloadData], вы применяете синхронизацию, но не сможете анимировать ячейки.

NSFetchedResultsController можно использовать для синхронизации добавления / удаления нескольких ячеек с анимацией. С вашей стороны потребуется немного изменить проводку, но не так сильно, как вы думаете. С его помощью вы можете управлять стилями анимации и даже массово изменять ячейки, сохраняя при этом свою таблицу нетронутой. NSFetchedResultsController будет прослушивать изменения в Core Data на основе предиката и отражать эти изменения в табличном представлении. Это означает, что у вас может быть несколько табличных представлений, отвечающих их собственным NSFetchedResultsController, без необходимости уведомлять их, и все будет оставаться синхронизированным.

Хорошее место для поиска скелетного кода - создание нового проекта Core Data в XCode. Методы, которые вам нужно добавить в свой код (находятся в MasterViewController.m):

// this references your controller that listens to Core Data and communicates with the tableview
- (NSFetchedResultsController *)fetchedResultsController 

// delegate methods:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
       atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type;
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
   atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
  newIndexPath:(NSIndexPath *)newIndexPath;
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;

Затем вашему tableview нужно будет проконсультироваться с экземпляром NSFetchedController для его методов делегата / источника данных. Например:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [[self.fetchedResultsController sections] count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section];
    return [sectionInfo numberOfObjects];
}

Если вы чувствуете себя достаточно продвинутым, есть отличный учебник, который показывает, как вы можете поставить в очередь добавления / удаления ячеек для повышения производительности:

http://www.fruitstandsoftware.com/blog/2013/02/uitableview-and-nsfetchedresultscontroller-updates-done-right/

NSFetchedResultsController - одна из тех вещей, о которых вы действительно не слышите в базовом руководстве по Core Data, но она оказывается реальным преимуществом при создании анимации таблиц и обработке больших объемов данных (посредством кэширования).

person Thomas Verbeek    schedule 17.02.2014
comment
Несколько человек порекомендовали NAFetchedResultsController, и в настоящее время я занимаюсь рефакторингом, чтобы использовать его. Спасибо за дополнительную информацию! - person Januzellij; 18.02.2014

Я использую следующий код всякий раз, когда пользователь перемещает ячейку, чтобы удалить ее:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        if (tableView == self.tableView)
        {
           NSInteger row = [indexPath row];
           Entry *e = self.entries[row];

           [self.entries removeObjectAtIndex: indexPath.row];

           [e MR_deleteEntity];
           [self.tableView reloadData];
        }
    }
}

В вашем случае вам сначала нужно выяснить, соответствует ли 'row' Note или Folder, но идея та же.

person koen    schedule 17.02.2014
comment
Вот что странно. Когда я просто удаляю объект из хранилища Core Data и массива, а затем reloadData, он работает нормально. Проблемы возникают только тогда, когда я использую deleteRowsAtIndexPaths. - person Januzellij; 17.02.2014
comment
Так почему вы используете deleteRowsAtIndexPaths? - person koen; 17.02.2014
comment
Это может показаться мелочным, но мне бы хотелось, чтобы анимация удалялась - person Januzellij; 17.02.2014
comment
Хех :) Может быть, вам поможет это решение: keithposehn. net / post / 12164756946 /. Я могу использовать это и сам! - person koen; 17.02.2014
comment
Значит, мне придется перейти на NSFetchedResultsController? - person Januzellij; 17.02.2014
comment
Я еще не пробовал, но думаю, вы можете получить от NSFetchedResultsController до MagicalRecord, а затем вы установите свой viewController в качестве его делегата. - person koen; 17.02.2014

Я починил! Вернее, NSFetchedResultsController исправил это. Я реализовал

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

на мой взгляд контроллер deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:. Мой метод удаления теперь просто

NSManagedObjectContext *context = [NSManagedObjectContext MR_defaultContext]; [object MR_deleteInContext:context]; [context MR_saveToPersistentStoreAndWait];

(спасибо за совет по поводу MR_contextForCurrentThread).

Вот loadData:

NSManagedObjectContext *c = [NSManagedObjectContext MR_defaultContext]; NSPredicate *textParentFilter = [NSPredicate predicateWithFormat:@"parent == %@", self.parent]; self.notes = [Note MR_fetchAllSortedBy:@"text" ascending:YES withPredicate:textParentFilter groupBy:nil delegate:self inContext:c]; NSPredicate *folderParentFilter = [NSPredicate predicateWithFormat:@"parentFolder == %@", self.parent]; self.folders = [Folder MR_fetchAllSortedBy:@"title" ascending:YES withPredicate:folderParentFilter groupBy:nil delegate:self inContext:c];

И - (NSInteger) tableView: (UITableView *) tableView numberOfRowsInSection: (NSInteger) раздел:

if ([self.notes.sections count] + [self.folders.sections count] > 0) { id <NSFetchedResultsSectionInfo> notesSectionInfo = [self.notes.sections objectAtIndex:section]; id <NSFetchedResultsSectionInfo> foldersSectionInfo = [self.folders.sections objectAtIndex:section]; return [notesSectionInfo numberOfObjects] + [foldersSectionInfo numberOfObjects]; } else return 0;

person Januzellij    schedule 18.02.2014