CollectionView + UIKit Dynamics se bloquea en performBatchUpdates:

Estoy atrapado con un extraño crash y tratando de solucionarlo todo el día. Tengo una UICollectionViewLayout personalizada que básicamente agrega gravedad y comportamiento de colisión a las celdas.

¡La implementación funciona muy bien! El problema ocurre cuando bash eliminar una celda usando: [self.collectionView performBatchUpdates:].

Me da el siguiente error:

2013-12-12 21:15:35.269 APPNAME[97890:70b] *** Assertion failure in -[UICollectionViewData validateLayoutInRect:], /SourceCache/UIKit_Sim/UIKit-2935.58/UICollectionViewData.m:357 2013-12-12 20:55:49.739 APPNAME[97438:70b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView recieved layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0x975d290> {length = 2, path = 0 - 4}' 

¡Mi model se está manejando correctamente y puedo ver cómo se elimina el elemento!

Las routes de índice del elemento a eliminar se pasan correctamente entre objects. La única vez que la actualización de collectionView no falla es cuando borro la última celda, de lo contrario, ocurre el locking.

Aquí está el código que estoy usando para eliminar la celda.

 - (void)removeItemAtIndexPath:(NSIndexPath *)itemToRemove completion:(void (^)(void))completion { UICollectionViewLayoutAttributes *attributes = [self.dynamicAnimator layoutAttributesForCellAtIndexPath:itemToRemove]; [self.gravityBehaviour removeItem:attributes]; [self.itemBehaviour removeItem:attributes]; [self.collisionBehaviour removeItem:attributes]; [self.collectionView performBatchUpdates:^{ [self.fetchedBeacons removeObjectAtIndex:itemToRemove.row]; [self.collectionView deleteItemsAtIndexPaths:@[itemToRemove]]; } completion:nil]; } 

Los delegates de CollectionView que maneja los attributes de las celdas son los básicos a continuación.

 - (CGSize)collectionViewContentSize { return self.collectionView.bounds.size; } - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { return [self.dynamicAnimator itemsInRect:rect]; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { return [self.dynamicAnimator layoutAttributesForCellAtIndexPath:indexPath]; } 

Cosas que ya intenté sin éxito: – Invalidar el layout – Recargar los datos – Eliminar los comportamientos del UIDynamicAnimator y agregarlos nuevamente después de la actualización

¿Alguna idea?

Un código fuente con el problema está disponible en este repository. Por favor, míralo. Repositorio de código

Mejor. Jorge.

Después de algunas semanas luchando con este error, algunas ideas útiles de nuestros amigos @ david-h y @erwin y algunas llamadas y correos electrónicos del WWDR de Apple, pude resolver este problema. Solo quiero compartir la solución con la comunidad.

  1. El problema. UIKit Dynamics tiene algunos methods de ayuda para trabajar con Vistas de Colección y estos methods en realidad no hacen algo interno que creo que debería hacer automáticamente, como actualizar automáticamente las celdas animadas dinámicamente cuando se realiza alguna acción usando performBatchUpdates: Por lo tanto, debe hacerlo manualmente de acuerdo con el WWDR, ​​por lo que iteraremos sobre los itens actualizados actualizando sus NSIndexPaths para que el animador dynamic nos pueda dar las actualizaciones adecuadas en las celdas.

  2. El bicho. Incluso haciendo esta actualización de indexPath en una inserción / eliminación de celda, tuve muchos choques aleatorios y comportamientos extraños con la animation. Entonces, seguí un consejo dado por @erwin que consiste en volver a crear una instancia del UIDynamicAnimator después de un performBatchUpdates: y eso corrige todos los problemas con este tipo de situación.

Entonces el codigo

 - (void)removeItemAtIndexPath:(NSIndexPath *)itemToRemove completion:(void (^)(void))completion { UICollectionViewLayoutAttributes *attributes = [self.dynamicAnimator layoutAttributesForCellAtIndexPath:itemToRemove]; if (attributes) { [self.collisionBehaviour removeItem:attributes]; [self.gravityBehaviour removeItem:attributes]; [self.itemBehaviour removeItem:attributes]; // Fix the problem explained on 1. // Update all the indexPaths of the remaining cells NSArray *remainingAttributes = self.collisionBehaviour.items; for (UICollectionViewLayoutAttributes *attributes in remainingAttributes) { if (attributes.indexPath.row > itemToRemove.row) attributes.indexPath = [NSIndexPath indexPathForRow:(attributes.indexPath.row - 1) inSection:attributes.indexPath.section]; } [self.collectionView performBatchUpdates:^{ completion(); [self.collectionView deleteItemsAtIndexPaths:@[itemToRemove]]; } completion:nil]; // Fix the bug explained on 2. // Re-instantiate the Dynamic Animator self.dynamicAnimator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self]; [self.dynamicAnimator addBehavior:self.collisionBehaviour]; [self.dynamicAnimator addBehavior:self.gravityBehaviour]; [self.dynamicAnimator addBehavior:self.itemBehaviour]; } } 

Abrí un radar explicando el problema esperando que Apple reparara esto en una futura actualización. Si alguien tiene que duplicarlo, está disponible en OpenRadar .

Gracias a todos.

He estado luchando con una situación similar.

En mi caso, estoy usando UIAttachmentBehaviors, por lo que cada elemento UICollectionViewLayoutAttributes obtiene su propio comportamiento. Entonces, en lugar de eliminar elementos de comportamientos, elimino el comportamiento apropiado del animador dynamic.

Para mí, eliminar un UICollectionViewCell medio parece funcionar (sin locking), pero la aplicación se bloquea si trato de eliminar la última celda.

Una inspección minuciosa de los comportamientos del animador (utilizando los loggings de debugging) muestra que las routes de índice de los elementos restantes están realmente desconectadas una vez pasado el elemento que se eliminó. El restablecimiento manual de ellos no soluciona el problema por sí mismo.

El problema parece ser una discrepancia entre el número de celdas en la vista de colección y el número de elementos devueltos por el animador dynamic -itsInRect: (todas mis celdas están siempre visibles).

Puedo evitar un locking eliminando todos los comportamientos antes de eliminar una celda. Pero, por supuesto, esto resulta en un movimiento no deseado de mis células cuando desaparecen los files adjuntos.

Lo que realmente necesitaba era una manera de restablecer los elementos en el animador dynamic sin descartarlos por completo y volver a crearlos.

Entonces, finalmente, se me ocurrió un mecanismo basado en almacenar los comportamientos a un lado, reincorporar un animador dynamic y volver a agregar los comportamientos.

Parece que funciona bien, y probablemente pueda optimizarse aún más.

 - (void)detachItemAtIndexPath:(NSIndexPath *)indexPath completion:(void (^)(void))completion { for (UIAttachmentBehavior *behavior in dynamicAnimator.behaviors) { UICollectionViewLayoutAttributes *thisItem = [[behavior items] firstObject]; if (thisItem.indexPath.row == indexPath.row) { [dynamicAnimator removeBehavior:behavior]; } if (thisItem.indexPath.row > indexPath.row) { thisItem.indexPath = [NSIndexPath indexPathForRow:thisItem.indexPath.row-1 inSection:0]; } } NSArray *tmp = [NSArray arrayWithArray:dynamicAnimator.behaviors]; self.dynamicAnimator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self]; for (UIAttachmentBehavior *behavior in tmp) { [dynamicAnimator addBehavior:behavior]; } // custom algorithm to place cells for (UIAttachmentBehavior *behavior in dynamicAnimator.behaviors) { [self setAnchorPoint:behavior]; } [self.collectionView performBatchUpdates:^{ [self.collectionView deleteItemsAtIndexPaths:@[indexPath]]; } completion:^(BOOL finished) { completion(); }]; 

}

Primero, ¡un proyecto increíble! Me encanta cómo rebotan las cajas. Parece que siempre falta uno: fila 0. De todos modos, ¡cualquiera que lea esto y tenga interés en el layout debería verlo! Me encanta la animation.

En … Layout.m:

Cambió este método para simplemente recargar:

 - (void)removeItemAtIndexPath:(NSIndexPath *)itemToRemove completion:(void (^)(void))completion { //assert([NSThread isMainThread]); UICollectionViewLayoutAttributes *attributes = [self.dynamicAnimator layoutAttributesForCellAtIndexPath:itemToRemove]; [self.collisionBehaviour removeItem:attributes]; [self.gravityBehaviour removeItem:attributes]; [self.itemBehaviour removeItem:attributes]; completion(); [self.collectionView reloadData]; } 

Agregué estos posts de logging:

 - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSLog(@"ASKED FOR LAYOUT ELEMENTS IN RECT"); NSArray *foo = [self.dynamicAnimator itemsInRect:rect]; NSLog(@"...LAYOUT ELEMENTS %@", foo); return foo; } 

Ejecutó el progtwig y eliminó un elemento central. Mire las routes de índice en la console. Cuando elimina un elemento, debe restablecer las routes de índice, ya que ahora no reflejan correctamente los nuevos índices de las celdas.

 CollectionViewDynamics[95862:70b] ASKED FOR LAYOUT ELEMENTS IN RECT CollectionViewDynamics[95862:70b] ...LAYOUT ELEMENTS ( "<UICollectionViewLayoutAttributes: 0x109405530> index path: (<NSIndexPath: 0xc000000000008016> {length = 2, path = 0 - 1}); frame = (105.41 -102.09; 100.18 100.18); transform = [0.99999838000043739, -0.0017999990280001574, 0.0017999990280001574, 0.99999838000043739, 0, 0]; ", "<UICollectionViewLayoutAttributes: 0x10939c2b0> index path: (<NSIndexPath: 0xc000000000018016> {length = 2, path = 0 - 3}); frame = (1 -100.5; 100 100); ", "<UICollectionViewLayoutAttributes: 0x10912b200> index path: (<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}); frame = (3 468; 100 100); " ) CollectionViewDynamics[95862:70b] *** Assertion failure in -[UICollectionViewData validateLayoutInRect:], /SourceCache/UIKit_Sim/UIKit-2935.80.1/UICollectionViewData.m:357 

Arreglar eso debería ayudarlo (espero). ¡NSLog ha sido mi mejor amigo durante años! YMMV

Solucionado un problema similar por una cadena

 self.<#custom_layout_class_name#>.dynamicAnimator = nil; 

Tiene que lanzarlo cada vez que renueva la fuente de datos.