IOS 9 - "попытка удалить и перезагрузить один и тот же путь индекса"
Это ошибка:
CoreData: ошибка: серьезная ошибка приложения. Исключение было обнаружено у делегата NSFetchedResultsController во время вызова -controllerDidChangeContent:. попытайтесь удалить и перезагрузить один и тот же путь индекса ({length = 2, path = 0 - 0}) с userInfo (null)
Это мой типичный NSFetchedResultsControllerDelegate
:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
let indexSet = NSIndexSet(index: sectionIndex)
switch type {
case .Insert:
tableView.insertSections(indexSet, withRowAnimation: .Fade)
case .Delete:
tableView.deleteSections(indexSet, withRowAnimation: .Fade)
case .Update:
fallthrough
case .Move:
tableView.reloadSections(indexSet, withRowAnimation: .Fade)
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: NSManagedObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
if let newIndexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
case .Delete:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
case .Update:
if let indexPath = indexPath {
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None)
}
case .Move:
if let indexPath = indexPath {
if let newIndexPath = newIndexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
}
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
в viewDidLoad()
:
private func setupOnceFetchedResultsController() {
if fetchedResultsController == nil {
let context = NSManagedObjectContext.MR_defaultContext()
let fetchReguest = NSFetchRequest(entityName: "DBOrder")
let dateDescriptor = NSSortDescriptor(key: "date", ascending: false)
fetchReguest.predicate = NSPredicate(format: "user.identifier = %@", DBAppSettings.currentUser!.identifier )
fetchReguest.sortDescriptors = [dateDescriptor]
fetchReguest.fetchLimit = 10
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchReguest, managedObjectContext: context, sectionNameKeyPath: "identifier", cacheName: nil)
fetchedResultsController.delegate = self
try! fetchedResultsController.performFetch()
}
}
Ответы
Ответ 1
По какой-то причине NSFetchedResultsController
вызывает .Update
, за которым следует .Move
после вызова controllerWillChangeContent:
.
Просто это выглядит так: НАЧАТЬ ОБНОВЛЕНИЯ → ОБНОВЛЕНИЕ → ПЕРЕМЕЩЕНИЕ → END UPDATES.
Случается только под iOS 8.x
Во время одного сеанса обновления одна и та же ячейка перезагружается и удаляется, что приводит к сбою.
ПРОСТОЙ ИСПРАВИТЬ ВСЕХ:
Следующая часть кода:
case .Update:
if let indexPath = indexPath {
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
заменить на:
case .Update:
if let indexPath = indexPath {
// 1. get your cell
// 2. get object related to your cell from fetched results controller
// 3. update your cell using that object
//EXAMPLE:
if let cell = tableView.cellForRowAtIndexPath(indexPath) as? WLTableViewCell { //1
let wishlist = fetchedResultsController.objectAtIndexPath(indexPath) as! WLWishlist //2
cell.configureCellWithWishlist(wishlist) //3
}
}
ЧТО ДЕЙСТВИТЕЛЬНО РАБОТАЕТ.
Ответ 2
Кажется, это ошибка в iOS 9 (которая все еще является бета-версией), а также обсуждается на форуме разработчиков Apple
Я могу подтвердить проблему с iOS 9 Simulator из Xcode 7 beta 3.
Я заметил, что для обновленного управляемого объекта метод делегата didChangeObject:
вызывается дважды: один раз с событием NSFetchedResultsChangeUpdate
, а затем снова с событием NSFetchedResultsChangeMove
(и indexPath == newIndexPath
).
Добавление явной проверки для indexPath != newIndexPath
как предложено в приведенном выше потоке, кажется, решает проблему:
case .Move:
if indexPath != newIndexPath {
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
}
Ответ 3
Обновление: описанная проблема возникает только на iOS 8 при создании против SDK iOS 9.0 или iOS 9.1 (beta).
Сегодня я столкнулся с каким-то ужасным обходным решением после игры с Xcode 7 beta 6 (iOS 9.0 beta 5), и кажется, что он работает.
Вы не можете использовать reloadRowsAtIndexPaths
, потому что в некоторых случаях он называется слишком ранним и может вызывать несогласованность, вместо этого вы должны вручную обновить свою ячейку.
Я все еще считаю, что лучший вариант - просто вызвать reloadData
.
Я считаю, что вы можете быстро адаптировать мой код для быстрой работы, у меня есть проект objective-c.
@property NSMutableIndexSet *deletedSections, *insertedSections;
// ...
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
self.deletedSections = [[NSMutableIndexSet alloc] init];
self.insertedSections = [[NSMutableIndexSet alloc] init];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndex:sectionIndex];
switch(type) {
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:indexSet withRowAnimation:UITableViewRowAnimationAutomatic];
[self.deletedSections addIndexes:indexSet];
break;
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:indexSet withRowAnimation:UITableViewRowAnimationAutomatic];
[self.insertedSections addIndexes:indexSet];
break;
default:
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
switch(type) {
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:@[ newIndexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
// iOS 9.0b5 sends the same index path twice instead of delete
if(![indexPath isEqual:newIndexPath]) {
[self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertRowsAtIndexPaths:@[ newIndexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
}
else if([self.insertedSections containsIndex:indexPath.section]) {
// iOS 9.0b5 bug: Moving first item from section 0 (which becomes section 1 later) to section 0
// Really the only way is to delete and insert the same index path...
[self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
}
else if([self.deletedSections containsIndex:indexPath.section]) {
// iOS 9.0b5 bug: same index path reported after section was removed
// we can ignore item deletion here because the whole section was removed anyway
[self.tableView insertRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
}
break;
case NSFetchedResultsChangeUpdate:
// On iOS 9.0b5 NSFetchedResultsController may not even contain such indexPath anymore
// when removing last item from section.
if(![self.deletedSections containsIndex:indexPath.section] && ![self.insertedSections containsIndex:indexPath.section]) {
// iOS 9.0b5 sends update before delete therefore we cannot use reload
// this will never work correctly but at least no crash.
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
[self _configureCell:cell forRowAtIndexPath:indexPath];
}
break;
}
}
Только Xcode 7/iOS 9.0
В Xcode 7/iOS 9.0 NSFetchedResultsChangeMove
по-прежнему отправляется вместо "обновления".
Как простой обходной путь, просто отключите анимацию для этого случая:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableViewRowAnimation animation = UITableViewRowAnimationAutomatic;
switch(type) {
case NSFetchedResultsChangeMove:
// @MARK: iOS 9.0 bug. Move sent instead of update. indexPath = newIndexPath.
if([indexPath isEqual:newIndexPath]) {
animation = UITableViewRowAnimationNone;
}
[self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:animation];
[self.tableView insertRowsAtIndexPaths:@[ newIndexPath ] withRowAnimation:animation];
break;
// ...
}
}
Ответ 4
Что касается этого события на iOS8, с сборками, собранными против iOS9, поверх проблемы indexPath==newIndexPath
, рассмотренной некоторыми другими ответами, происходит что-то еще, что очень странно.
Перечисление NSFetchedResultsChangeType
имеет четыре возможных значения (комментарии со значениями мои):
public enum NSFetchedResultsChangeType : UInt {
case Insert // 1
case Delete // 2
case Move // 3
case Update // 4
}
.. однако функция controller:didChangeObject:atIndexPath:forChangeType
иногда вызывается с недопустимым значением 0x0
.
Swift кажется по умолчанию для первого случая switch
в этой точке, поэтому, если у вас есть следующая структура:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert: tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case .Delete: tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case .Update: tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.None)
case .Move: tableView.moveRowAtIndexPath(ip, toIndexPath: nip)
}
}
.. неправильный вызов приведет к вставке, и вы получите сообщение об ошибке, например:
Неверное обновление: недопустимое количество строк в разделе 0. Количество строки, содержащиеся в существующем разделе после обновления (7), должны быть равное количеству строк, содержащихся в этом разделе, до update (7), плюс или минус количество строк, вставленных или удаленных из этот раздел (1 вставлен, 0 удален)
Просто замените случаи, чтобы первый случай был довольно безобидным. Обновление исправляет проблему:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Update: tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.None)
case .Insert: tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case .Delete: tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case .Move: tableView.moveRowAtIndexPath(ip, toIndexPath: nip)
}
}
Другим вариантом будет проверка type.rawValue
на недопустимое значение.
Примечание: в то время как это обращается к немного другому сообщению об ошибке, чем сообщение, отправленное OP, проблема связана; несколько вероятно, что как только вы исправите проблему indexPath==newIndexPath
, этот появится.
Кроме того, приведенные выше кодовые блоки упрощены для иллюстрации последовательности; например, отсутствуют соответствующие блоки guard
, пожалуйста, не используйте их как есть.
Кредиты: это было первоначально обнаружено iCN7, source: Форумы разработчиков Apple - обновление iOS 9 CoreData NSFetchedResultsController вызывает пустые строки в UICollectionView/UITableView
Ответ 5
Другие ответы были близки мне, но я получал "<invalid> (0x0)" как NSFetchedResultsChangeType. Я заметил, что это интерпретируется как изменение "вставки". Итак, для меня работало следующее исправление:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
// iOS 9 / Swift 2.0 BUG with running 8.4
if indexPath == nil {
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
}
(etc...)
}
Так как каждая "вставка" возвращается только с помощью newIndexPath и без indexPath (и этот странный дополнительный делегат-вызов вставки возвращается с тем же путем, что и для newIndexPath и indexPath), это просто проверяет, что это правильный тип "вставить" и пропустить остальные.
Ответ 6
Проблема возникла из-за перезагрузки и удаления той же indexPath (которая была вызвана ядром), поэтому я изменяю способ обработки сообщения NSFetchedResultsChangeUpdate
.
Вместо:
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
Я обновил содержимое ячейки вручную:
MyChatCell *cell = (MyChatCell *)[self.tableView cellForRowAtIndexPath:indexPath];
CoreDataObject *cdo = [[self fetchedResultsController] objectAtIndexPath:indexPath];
// update the cell with the content: cdo
[cell updateContent:cdo];
Оказывается, хорошо работает.
BTW:
обновление объекта CoreData приведет к удалению и вставке сообщения. Чтобы правильно обновить содержимое ячейки, когда indexPath
равен newIndexPath
(оба раздела и строка равны), я перезаряжаю ячейку
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
Вот пример кода:
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
if (![self isViewLoaded]) return;
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:{
MyChatCell *cell = (MyChatCell *)[self.tableView cellForRowAtIndexPath:indexPath];
CoreDataObject *cdo = [[self fetchedResultsController] objectAtIndexPath:indexPath];
// update the cell with the content: cdo
[cell updateContent:cdo];
}
break;
case NSFetchedResultsChangeMove:
if (indexPath.row!=newIndexPath.row || indexPath.section!=newIndexPath.section){
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
}else{
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
}
}
Я поставил выше примерный код:
https://gist.github.com/dreamolight/157266c615d4a226e772