ios swift ios9 nsfetchedresultscontroller

iOS 9: "intento de eliminar y volver a cargar la misma ruta de índice"



swift ios9 (6)

Actualización: el problema descrito ocurre solo en iOS 8 cuando compila contra iOS 9.0 o iOS 9.1 (beta) SDK.

Se me ocurrió una solución horrible hoy después de jugar con Xcode 7 beta 6 (iOS 9.0 beta 5) y parece que funciona.

No puede usar reloadRowsAtIndexPaths porque en algunos casos se llama demasiado pronto y puede causar inconsistencias, en su lugar debe actualizar manualmente su celda.

Sigo pensando que la mejor opción es simplemente llamar a reloadData .

Creo que puedes adaptar mi código rápidamente sin ningún esfuerzo, aquí tengo el proyecto ObjectC.

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

Solo Xcode 7 / iOS 9.0

En Xcode 7 / iOS 9.0 NSFetchedResultsChangeMove todavía se está enviando en lugar de "actualizar".

Como solución simple, solo desactive las animaciones para ese caso:

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

Esto es un error:

CoreData: error: error grave de la aplicación. Se capturó una excepción del delegado de NSFetchedResultsController durante una llamada a -controllerDidChangeContent :. Intentar eliminar y volver a cargar la misma ruta de índice ({length = 2, path = 0 - 0}) con userInfo (null)

Este es mi típico 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() }

en 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() } }


Las otras respuestas estaban cerca para mí, pero recibía "<inválido> (0x0)" como NSFetchedResultsChangeType. Noté que se interpretaba como un cambio de "inserción". Entonces la siguiente solución funcionó para mí:

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...) }

Como cada "inserción" solo vuelve con un NewIndexPath y sin indexPath (y esta extraña llamada de delegado de inserción adicional vuelve con la misma ruta enumerada tanto para newIndexPath como indexPath), esto solo comprueba que es el tipo correcto de "inserción" y se salta a los demás.


El problema ocurrió debido a volver a cargar y eliminar el mismo indexPath (que es un error producido por Apple), por lo que NSFetchedResultsChangeUpdate la forma en que manejo el mensaje NSFetchedResultsChangeUpdate .

En lugar de:

[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

Actualicé el contenido de la célula manualmente:

MyChatCell *cell = (MyChatCell *)[self.tableView cellForRowAtIndexPath:indexPath]; CoreDataObject *cdo = [[self fetchedResultsController] objectAtIndexPath:indexPath]; // update the cell with the content: cdo [cell updateContent:cdo];

Resulta que está funcionando bien.

Por cierto: la actualización del objeto CoreData produciría una eliminación y un mensaje de inserción. Para actualizar el contenido de la celda correctamente, cuando indexPath es igual al newIndexPath (tanto la sección como la fila son iguales), recargo la celda
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

Aquí está el código de ejemplo:

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

Puse el código de ejemplo arriba a la esencia: https://gist.github.com/dreamolight/157266c615d4a226e772


Con respecto a esto sucede en iOS8, con compilaciones compiladas contra iOS9 , además del problema indexPath==newIndexPath abordado por algunas otras respuestas, ocurre algo más que es muy extraño .

La enumeración NSFetchedResultsChangeType tiene cuatro valores posibles (los comentarios con valores son míos ):

public enum NSFetchedResultsChangeType : UInt { case Insert // 1 case Delete // 2 case Move // 3 case Update // 4 }

.. sin embargo, el controller:didChangeObject:atIndexPath:forChangeType función a veces se llama con un valor no válido 0x0 .

Swift parece estar predeterminado a la primera caja de switch en ese punto, por lo que si tiene la siguiente estructura:

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) } }

.. la llamada no válida dará como resultado un Insertar, y obtendrá un error como el siguiente:

Actualización inválida: número inválido de filas en la sección 0. El número de filas contenidas en una sección existente después de la actualización (7) debe ser igual al número de filas contenidas en esa sección antes de la actualización (7), más o menos el número de filas insertadas o eliminadas de esa sección (1 insertado, 0 eliminado)

Simplemente intercambiando los casos para que el primer caso sea una actualización bastante inofensiva corrige el problema:

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) } }

Otra opción sería verificar type.rawValue para un valor no válido.

Nota : si bien se trata de un mensaje de error ligeramente diferente al publicado por el OP, el problema está relacionado; es bastante probable que tan pronto como arregle el problema indexPath==newIndexPath , este aparezca. Además, los bloques de código anteriores se simplifican para ilustrar la secuencia; los bloques de guard apropiados faltan, por ejemplo, por favor no los use tal como están.

Créditos : esto fue descubierto originalmente por iCN7, fuente: Foros de desarrolladores de Apple : actualización de iOS 9 CoreData NSFetchedResultsController causa filas en blanco en UICollectionView / UITableView


Por algún motivo, NSFetchedResultsController llama a .Update seguido de .Move después de .Move controllerWillChangeContent:

Simplemente se ve así: COMIENCE ACTUALIZACIONES -> ACTUALIZACIÓN -> MOVER -> FIN ACTUALIZACIONES .

Ocurre solo en iOS 8.x

Durante una sesión de actualización, la misma celda se vuelve a cargar y se elimina, lo que causa un bloqueo.

EL ARREGLO MÁS FÁCIL DE LA VIDA:

La siguiente parte del código:

case .Update: if let indexPath = indexPath { tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) }

reemplazar con:

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

QUE REALMENTE FUNCIONA .


Esto parece ser un error en iOS 9 (que sigue siendo beta) y también se discute en el Apple Developer Forum.

Puedo confirmar el problema con el simulador de iOS 9 de Xcode 7 beta 3. didChangeObject: que para un objeto gestionado actualizado, el método didChangeObject: delegate se llama dos veces: una vez con el evento NSFetchedResultsChangeUpdate y luego otra vez con el evento NSFetchedResultsChangeMove (y indexPath == newIndexPath ).

Agregar una comprobación explícita para indexPath != newIndexPath como se sugiere en el hilo anterior parece resolver el problema:

case .Move: if indexPath != newIndexPath { tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade) tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade) }