Динамически изменяющийся источник данных, вызывающий deleteRowsAtIndexPaths: индексы для сбоя
Разрывая мои волосы, пытаясь заставить это работать. Я хочу выполнить [self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
,
Более подробный код, как я удаляю:
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
Это прекрасно работает, если я получаю push-уведомление (то есть новое сообщение, полученное), в то время как удаление приложения приведет к сбою и отображению ошибки, например:
Ошибка подтверждения в - [UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit -3347.44/UITableView.m:1327 2015-07-04 19: 12: 48.623 myapp [319: 24083] *** Завершение приложения из-за неперехваченного исключения "NSInternalInconsistencyException", причина: 'попытка удалить строку 1 из раздела 0, которая содержит только 1 строку перед обновлением '
Я подозреваю, что это связано с тем, что меняет источник данных, размер массива, который
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
ссылки при удалении не будут согласованы, потому что они были увеличены на единицу, когда push-уведомление вызвало обновление. Есть ли способ обойти это? Правильно ли, что deleteRowsAtIndexPaths
использует метод numberOfRowsInSection
?
Ответы
Ответ 1
Итак, чтобы решить вашу проблему, вам необходимо убедиться, что ваш источник данных не изменится, пока есть анимация в виде таблицы. Я бы предложил сделать следующее.
Сначала создайте два массива: messagesToDelete
и messagesToInsert
. Они будут содержать информацию о том, какие сообщения вы хотите удалить/вставить.
Во-вторых, добавьте логическое свойство updatingTable
в источник данных таблицы просмотра.
В-третьих, добавьте следующие функции:
-(void)updateTableIfPossible {
if (!updatingTable) {
updatingTable = [self updateTableViewWithNewUpdates];
}
}
-(BOOL)updateTableViewWithNewUpdates {
if ((messagesToDelete.count == 0)&&(messagesToInsert.count==0)) {
return false;
}
NSMutableArray *indexPathsForMessagesThatNeedDelete = [[NSMutableArray alloc] init];
NSMutableArray *indexPathsForMessagesThatNeedInsert = [[NSMutableArray alloc] init];
// for deletion you need to use original messages to ensure
// that you get correct index paths if there are multiple rows to delete
NSMutableArray *oldMessages = [self.messages copy];
for (id message in messagesToDelete) {
int index = (int)[self.oldMessages indexOfObject:message];
[self.messages removeObject:message];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
[indexPathsForMessagesThatNeedDelete addObject:indexPath];
}
for (id message in messagesToInsert) {
[self.messages insertObject:message atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[indexPathsForMessagesThatNeedInsert addObject:indexPath];
}
[messagesToDelete removeAllObjects];
[messagesToInsert removeAllObjects];
// at this point your messages array contains
// all messages which should be displayed at CURRENT time
// now do the following
[CATransaction begin];
[CATransaction setCompletionBlock:^{
updatingTable = NO;
[self updateTableIfPossible];
}];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:indexPathsForMessagesThatNeedDelete withRowAnimation:UITableViewRowAnimationLeft];
[tableView insertRowsAtIndexPaths:indexPathsForMessagesThatNeedInsert withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
[CATransaction commit];
return true;
}
Наконец, вам нужно иметь следующий код во всех функциях, которые хотят добавлять/удалять строки.
Чтобы добавить сообщение
[self.messagesToInsert addObject:message];
[self updateTableIfPossible];
Чтобы удалить сообщение
[self.messagesToDelete addObject:message];
[self updateTableIfPossible];
Что делает этот код, это обеспечивает стабильность вашего источника данных. Всякий раз, когда происходит изменение, вы добавляете сообщения, которые необходимо вставить/удалить в массивы (messagesToDelete
и messagesToDelete
). Затем вы вызываете функцию updateTableIfPossible
, которая будет обновлять источник данных представления таблицы (и будет анимировать изменение) при условии, что текущая анимация не выполняется. Если анимация продолжается, на данном этапе ничего не будет сделано.
Однако, потому что мы добавили завершение
[CATransaction setCompletionBlock:^{
updatingTable = NO;
[self updateTableIfPossible];
}];
в конце анимации наш источник данных проверяет, есть ли какие-либо новые изменения, которые необходимо применить к представлению таблицы, и если да, то он обновит анимацию.
Это гораздо более безопасный способ обновления источника данных. Пожалуйста, дайте мне знать, если это сработает для вас.
Ответ 2
Удалить строку
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
Удалить раздел
Примечание: если ваш TableView имеет несколько разделов, вам нужно удалить весь раздел, если раздел содержит только одну строку вместо удаления строки
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexSet indexSetWithIndex:0];
[tableView beginUpdates];
[tableView deleteSections:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
Ответ 3
У меня есть несколько тестов о вашем коде выше. И вы не можете сделать этого в то же самое время, что меньше всего мы можем сделать, это что-то вроде:
//This will wait for `deleteRowsAtIndexPaths:indexes` before the `setCompletionBlock `
//
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[tableView reloadData];
}];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
[CATransaction commit];
В вашем коде есть:
/*
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
*/
Это небезопасно, потому что если при запуске уведомления и по какой-либо причине [self.tableView reloadData];
, вызванной непосредственно перед deleteRowsAtIndexPaths
, что приведет к сбою, поскольку tableView в настоящее время обновляет данные, а затем прерывает deleteRowsAtIndexPaths:
, попробуйте эту последовательность для проверки:
/*
...
[self.tableView reloadData];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
This will cause a crash...
*/
Хмм.. Вернемся к вашему коду. Пусть у вас есть симуляция, которая может привести к сбою. ЭТО - просто предположение, хотя это не на 100% (99%).:)
Предположим, что self.messageToDelete
равно nil;
int index = (int)[self.messages indexOfObject: nil];
// since you casted this to (int) with would be 0, so `index = 0`
[self.messages removeObject: nil];
// self.messages will remove nil object which is non existing therefore
// self.messages is not updated/altered/changed
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
// indexPath.row == 0
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
// then you attempted to delete index indexPath at row 0 in section 0
// but the datasource is not updated meaning this will crash..
//
// Same thing will happen if `self.messageToDelete` is not existing in your `self.messages `
Мое предложение - сначала проверить self.messageToDelete
:
if ([self.messages containsObject:self.messageToDelete])
{
// your delete code...
}
Надеюсь, это полезно, Cheers!;)
Ответ 4
'попытается удалить строку 1 из раздела 0, которая содержит только 1 строку перед обновлением'
Это говорит, что ваш путь указателя во время удаления ссылается на строку 1. Однако строка 1 не существует. Существует только строка 0 (например, раздел, они основаны на нуле).
Итак, как вы получаете indexPath больше, чем количество элементов?
Можно ли увидеть обработчик уведомлений, который вставляет строку? Вы можете сделать какой-то взлом, где, если анимация находится в процессе, вы выполняетеSelector: withDelay: во время обработки уведомлений, чтобы дать время для анимации.
Ответ 5
В вашем коде причина сбоя: вы обновляете массив, но не обновляете источник данных UITableView
. поэтому ваш источник данных также должен отражать изменения к времени, которое вызывается endUpdates.
Итак, согласно документации Apple.
ManageInsertDeleteRow
Чтобы оживить вставку, удаление и перезагрузку строк и разделов, вызовите соответствующие методы в блоке анимации, определяемом последовательными вызовами beginUpdates и endUpdates. Если вы не вызываете методы вставки, удаления и перезагрузки в этом блоке, индексы строк и секций могут быть недействительными. Звонки на beginUpdates и endUpdates могут быть вложенными; все индексы обрабатываются так, как если бы был только внешний блок обновления.
В конце блока, то есть после возврата endUpdates - представление таблицы запрашивает свой источник данных и делегирует, как обычно, данные строки и раздела. Таким образом, объекты коллекции, поддерживающие представление таблицы, должны обновляться, чтобы отражать новые или удаленные строки или разделы.
Пример:
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
Надеюсь, что это поможет вам.
Ответ 6
Я думаю, что ответ упоминается во многих комментариях здесь, но я попробую немного поддразнивать его.
Первая проблема заключается в том, что это не похоже на то, что вы привязываете вызов метода удаления в методах beginUpdates
/endUpdates
. Это первое, что я хотел бы исправить, иначе, как предупреждается в документации Apple, все может пойти не так. (Это не значит, что они пойдет не так, вы просто более безопасно это делаете.)
Как только вы это сделали, важно то, что вызов endUpdates
проверяет источник данных, чтобы убедиться, что любые вставки или удаления учитываются при вызове numberOfRows
. Например, если вы удаляете строку (как вы это делаете), когда вы вызываете endUpdates
, лучше всего убедиться, что вы также удалили элемент из источника данных. Похоже, что вы делаете эту часть правильно, потому что вы удаляете элемент из self.messages
, который, как я полагаю, является вашим источником данных.
(Кстати, вы правы, что вызов deleteRows...
сам по себе без брекетинга в beginUpdates
/endUpdates
также вызывает numberOfRows
в источнике данных, который вы можете легко проверить, поставив в него точку останова. опять же, не делайте этого, всегда используйте beginUpdates
/endUpdates
.)
Теперь возможно, что между вызовами deleteRows...
и endUpdates
кто-то модифицирует массив self.messages
, добавляя к нему объект, но я не знаю, действительно ли я этому верю, потому что это должно быть крайне к сожалению приурочен. Скорее всего, когда вы получаете push-сообщение, вы неправильно обрабатываете ввод строки, опять же, не используя beginUpdates
/endUpdates
, или делая что-то еще неправильно. Было бы полезно, если бы вы могли опубликовать часть кода, который обрабатывает вставку.
Если эта часть выглядит нормально, похоже, что вы, к сожалению, вносите изменения в массив self.messages
в другом потоке, пока вызывается код удаления. Вы можете проверить это, добавив строку журнала, где ваш код вставки добавляет сообщение в массив:
NSLog(@"Running on %@ thread", [NSThread currentThread]);
или просто поместить в него точку останова и посмотреть, в какой поток вы попадаете. Если это действительно проблема, вы можете отправить код, который модифицирует массив, и вставляет строку в основной поток (где это должно быть так или иначе, поскольку это операция UI), выполнив что-то вроде ниже:
dispatch_async(dispatch_get_main_queue(), ^{
[self.messages addObject:newMessage];
[self.tableView beginUpdates];
[self.tableView insertRows...]
[self.tableView endUpdates];
});
Это гарантирует, что массив сообщений не будет мутирован другим потоком, в то время как основной поток занят удалением строки.