El desplazamiento a elemento animado en UICollectionView no siempre funciona

Problema

Me gustaría hacer que UICollectionView haga un desplazamiento animado a un elemento específico.

Esto funciona la mayor parte del time, pero de vez en cuando el object al que bash desplazarme no termina de mostrarse .

Código

- (void)onClick { // (Possibly recompute the _items array.) NSInteger target_idx = // (...some valid index of _items) NSIndexPath *item_idx = [NSIndexPath indexPathForItem:target_idx inSection:0]; [self scrollToItem:item_idx]; } - (void)scrollToItem:(NSIndexPath*)item_idx { // Make sure our view is up-to-date with the data we want to show. NSInteger num_items = [self.collection_view numberOfItemsInSection:0]; if (num_items != _items.count) { [self.collection_view reloadData]; } [self.collection_view scrollToItemAtIndexPath:item_idx atScrollPosition:UICollectionViewScrollPositionCentenetworkingHorizontally animated:YES]; } 

Detalles

  • self.collection_view es una UICollectionView que consta de una sola fila de elementos, con un layout de flujo estándar y un desplazamiento horizontal habilitado.
  • Necesito llamar a reloadData antes de desplazarme, porque es posible que _items haya cambiado desde que UICollectionView lo mostró por última vez.
  • El problema solo ocurre con el desplazamiento animado; si paso animated:NO , todo funciona como se esperaba.
  • Cuando ocurre el problema, una llamada posterior a desplazamiento a indexPathsForVisibleItems revela que UICollectionView no cree que el elemento de destino sea visible .

¿Algunas ideas por las que el desplazamiento a un elemento falla silenciosamente algunas veces?


Actualización: el problema parece provenir de la recarga y el desplazamiento en rápida sucesión; Si no hay recarga, el desplazamiento se comporta como se esperaba. ¿Hay expresiones idiomáticas para desplazarse a un elemento después de cargar nuevos datos?

Acabo de llegar a un problema similar / el mismo después de agregar un elemento a UICollectionView .

Que está pasando

El problema parece ser que inmediatamente después de [collectionView reloadData] o [collectionView insertItemsAtIndexPaths: @[newItemIndexPath]] , el tamaño de contenido de la vista de colección todavía no está actualizado .

Si luego intenta desplazar el elemento agregado visible , fallará porque el contenido rect aún no incluye espacio para el nuevo elemento .

Un arreglo

Hay un trabajo simple y bastante robusto, que consiste en publicar el evento de desplazamiento en la siguiente iteración del ciclo de ejecución de la siguiente manera:

 const NSUInteger newIndex = [self collectionView: self.collectionView numberOfItemsInSection: 0] - 1; NSIndexPath *const newPath = [NSIndexPath indexPathForItem: newIndex inSection: 0]; [self.collectionView insertItemsAtIndexPaths: @[newPath]]; UICollectionViewLayoutAttributes *const layoutAttributes = [self.collectionView layoutAttributesForItemAtIndexPath: newPath]; dispatch_async(dispatch_get_main_queue(), ^{ [self.collectionView scrollRectToVisible: layoutAttributes.frame animated: YES]; }); 

¿No hay una solución mejor?

Mientras funciona, esta "publicación de la llamada de desplazamiento en el próximo bucle de ejecución tic" las travesuras se siente hacky.

Sería preferible que UICollectionView pueda invocar una callback cuando termine de actualizar el contenido rect. UIKit tiene devoluciones de llamada de este estilo para otros methods que realizan actualizaciones asíncronas a su model. Por ejemplo, un bloque de finalización para UIViewController transiciones UIView y UIView animaciones UIView .

UICollectionView no proporciona esta callback. Por lo que sé, no hay otra forma simple de encontrar cuando completa su actualización. La próxima marca de ciclo de ejecución es un proxy para la callback que realmente queremos.

¿Algo más para saber?

Probablemente sea útil saber que UITableView también tiene este problema. Una solución similar debería funcionar allí también.

Con la ayuda de @NicholasHart, creo que entiendo el problema.

Intentar reloadData a reloadData y luego realizar un desplazamiento animado a una nueva position tiene sentido (y parece funcionar) siempre que la recarga haga que la colección sea más grande .

Cuando la recarga networkinguce la colección, sin embargo, el punto de inicio de un desplazamiento animado podría no existir más. Esto hace que la animation sea problemática.

Por ejemplo, si comienza con la vista desplazada todo el path a la derecha (para que el elemento más a la derecha esté visible) y luego la recargue y pierda la mitad de los elementos, no está claro cuál debería ser el punto de partida de la animation. Tratar de hacer un desplazamiento animado en esta situación da como resultado un no-op o un salto a una position muy extraña.

Una solución que parece razonablemente buena es solo animar si la colección se hace más grande:

 - (void)scrollToItem:(NSIndexPath*)item_idx { // Make sure our view is up-to-date with the data we want to show. NSInteger old_num_items = [self.collection_view numberOfItemsInSection:0]; NSInteger new_num_items = _items.count; if (old_num_items != new_num_items) { [self.collection_view reloadData]; } // Animating if we're getting smaller doesn't really make sense, and doesn't // seem to be supported. BOOL is_expanding = new_num_items >= old_num_items; [self.collection_view scrollToItemAtIndexPath:item_idx atScrollPosition:UICollectionViewScrollPositionCentenetworkingHorizontally animated:is_expanding]; }