La configuración de configuración dinámica en UICollectionView causa un contenido inexplicable

Según la documentación de Apple (y promocionado en la WWDC 2012), es posible establecer el diseño en UICollectionView forma dinámica e incluso animar los cambios:

Normalmente se especifica un objeto de disposición al crear una vista de colección, pero también se puede cambiar dinámicamente el diseño de una vista de colección. El objeto de disposición se almacena en la propiedad collectionViewLayout. Establecer esta propiedad actualiza directamente el diseño inmediatamente, sin animar los cambios. Si desea animar los cambios, debe llamar al método setCollectionViewLayout: animado: en su lugar.

Sin embargo, en la práctica, he descubierto que UICollectionView realiza UICollectionView inexplicables e incluso no válidos en contentOffset , lo que hace que las celdas se muevan incorrectamente, lo que hace que la característica sea prácticamente inutilizable. Para ilustrar el problema, armé el siguiente código de ejemplo que se puede adjuntar a un controlador de vista de colección predeterminado que se incluye en un guión gráfico:

 #import  @interface MyCollectionViewController : UICollectionViewController @end @implementation MyCollectionViewController - (void)viewDidLoad { [super viewDidLoad]; [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"CELL"]; self.collectionView.collectionViewLayout = [[UICollectionViewFlowLayout alloc] init]; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return 1; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:@"CELL" forIndexPath:indexPath]; cell.backgroundColor = [UIColor whiteColor]; return cell; } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y); [self.collectionView setCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init] animated:YES]; NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y); } @end 

El controlador establece un UICollectionViewFlowLayout predeterminado en viewDidLoad y muestra una sola celda en la pantalla. Cuando se seleccionan las celdas, el controlador crea otro UICollectionViewFlowLayout predeterminado y lo establece en la vista de colección con el indicador animated:YES . El comportamiento esperado es que la celda no se mueve. El comportamiento real, sin embargo, es que la celda se desplaza fuera de la pantalla, momento en el cual ni siquiera es posible desplazar la celda hacia atrás en la pantalla.

Mirar el registro de la consola revela que el contentOffset ha cambiado inexplicablemente (en mi proyecto, de (0, 0) a (0, 205)). Publiqué una solución para la solución para el caso no animado ( ie animated:NO ), pero como necesito animación, estoy muy interesado en saber si alguien tiene una solución o una solución para el caso animado.

Como nota al margen, probé diseños personalizados y obtuve el mismo comportamiento.

He estado tirando de mi cabello por esto durante días y he encontrado una solución para mi situación que puede ayudar. En mi caso, tengo un diseño de foto colapsada, como en la aplicación de fotos en el ipad. Muestra álbumes con las fotos una encima de la otra y cuando toca un álbum expande las fotos. Entonces lo que tengo es dos UICollectionViewLayouts separados y estoy alternando entre ellos con [self.collectionView setCollectionViewLayout:myLayout animated:YES] Estaba teniendo tu problema exacto con las células saltando antes de la animación y me di cuenta de que era el contentOffset . Intenté todo con contentOffset pero saltó durante la animación. La solución de tyler anterior funcionó, pero todavía estaba jugando con la animación.

Luego noté que solo ocurre cuando había algunos álbumes en la pantalla, pero no suficientes para llenar la pantalla. Mi diseño anula -(CGSize)collectionViewContentSize como se recomienda. Cuando solo hay unos pocos álbumes, el tamaño del contenido de la vista de colección es menor que el tamaño del contenido de las vistas. Eso está causando el salto cuando alterno entre los diseños de la colección.

Así que establecí una propiedad en mis diseños llamada minHeight y la establecí en la altura de la vista de colección de los padres. Luego -(CGSize)collectionViewContentSize la altura antes de volver a -(CGSize)collectionViewContentSize me -(CGSize)collectionViewContentSize que la altura sea> = la altura mínima.

No es una solución verdadera, pero está funcionando bien ahora. Intentaría configurar el contentSize del contentSize de su vista de colección para que sea al menos la longitud de su vista.

editar: Manicaesar agregó una solución fácil si heredas de UICollectionViewFlowLayout:

 -(CGSize)collectionViewContentSize { //Workaround CGSize superSize = [super collectionViewContentSize]; CGRect frame = self.collectionView.frame; return CGSizeMake(fmaxf(superSize.width, CGRectGetWidth(frame)), fmaxf(superSize.height, CGRectGetHeight(frame))); } 

UICollectionViewLayout contiene el método targetContentOffsetForProposedContentOffset: que le permite proporcionar el desplazamiento de contenido adecuado durante un cambio de diseño, y esto se animará correctamente. Esto está disponible en iOS 7.0 y superior

Este problema también me mordió y parece ser un error en el código de transición. Por lo que puedo decir, trata de enfocarse en la celda que estaba más cerca del centro del diseño de la vista anterior a la transición. Sin embargo, si no sucede que haya una celda en el centro de la pretransición de la vista, entonces todavía intenta centrarse donde la celda estaría después de la transición. Esto es muy claro si establece alwaysBounceVertical / Horizontal en YES, carga la vista con una sola celda y luego realiza una transición de diseño.

Pude evitar esto al decirle explícitamente a la colección que se concentre en una celda específica (la primera celda visible de la celda, en este ejemplo) después de activar la actualización del diseño.

 [self.collectionView setCollectionViewLayout:[self generateNextLayout] animated:YES]; // scroll to the first visible cell if ( 0 < self.collectionView.indexPathsForVisibleItems.count ) { NSIndexPath *firstVisibleIdx = [[self.collectionView indexPathsForVisibleItems] objectAtIndex:0]; [self.collectionView scrollToItemAtIndexPath:firstVisibleIdx atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES]; } 

Saltando con una respuesta tardía a mi propia pregunta.

La biblioteca TLLayoutTransitioning proporciona una gran solución a este problema al volver a realizar la tarea de las API de transición interactiva de iOS7 para realizar transiciones de diseño a diseño no interactivas. Proporciona una alternativa efectiva a setCollectionViewLayout , soluciona el problema de compensación de contenido y agrega varias características:

  1. Duración de la animación
  2. Más de 30 curvas de relajación (cortesía de la biblioteca AHEasing de Warren Moore)
  3. Modos de compensación de contenido múltiple

Las curvas de aceleración personalizadas se pueden definir como funciones AHEasingFunction . El desplazamiento de contenido final se puede especificar en términos de una o más rutas de índice con opciones de colocación mínima, central, superior, izquierda, inferior o derecha.

Para ver lo que quiero decir, intente ejecutar la demostración de Redimensionar en el espacio de trabajo Ejemplos y jugar con las opciones.

El uso es así. Primero, configure su controlador de vista para devolver una instancia de TLTransitionLayout :

 - (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout { return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout]; } 

Luego, en lugar de llamar a setCollectionViewLayout , llame a transitionToCollectionViewLayout:toLayout definido en la categoría UICollectionView-TLLayoutTransitioning :

 UICollectionViewLayout *toLayout = ...; // the layout to transition to CGFloat duration = 2.0; AHEasingFunction easing = QuarticEaseInOut; TLTransitionLayout *layout = (TLTransitionLayout *)[collectionView transitionToCollectionViewLayout:toLayout duration:duration easing:easing completion:nil]; 

Esta llamada inicia una transición interactiva e, internamente, una CADisplayLink llamada CADisplayLink que impulsa el progreso de la transición con la duración especificada y la función de aceleración.

El siguiente paso es especificar una compensación de contenido final. Puede especificar cualquier valor arbitrario, pero el método toContentOffsetForLayout definido en UICollectionView-TLLayoutTransitioning proporciona una forma elegante de calcular las compensaciones de contenido en relación con una o más rutas de índice. Por ejemplo, para que una celda específica termine lo más cerca posible del centro de la vista de recostackción, realice la siguiente llamada inmediatamente después de transitionToCollectionViewLayout :

 NSIndexPath *indexPath = ...; // the index path of the cell to center TLTransitionLayoutIndexPathPlacement placement = TLTransitionLayoutIndexPathPlacementCenter; CGPoint toOffset = [collectionView toContentOffsetForLayout:layout indexPaths:@[indexPath] placement:placement]; layout.toContentOffset = toOffset; 

Fácil.

Anima tu nuevo diseño y contentOffset de collectionViewView en el mismo bloque de animación.

 [UIView animateWithDuration:0.3 animations:^{ [self.collectionView setCollectionViewLayout:self.someLayout animated:YES completion:nil]; [self.collectionView setContentOffset:CGPointMake(0, -64)]; } completion:nil]; 

self.collectionView.contentOffset constante la constante self.collectionView.contentOffset .

veloz 3 .

Subclase UICollectionViewLayout . La forma en que cdemiris99 sugirió:

 override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint { return collectionView?.contentOffset ?? CGPoint.zero } 

Lo probé yo mismo usando mi propio diseño personalizado

Si simplemente busca la compensación de contenido para que no cambie durante la transición desde los diseños, puede crear un diseño personalizado y anular un par de métodos para realizar un seguimiento del viejo contentOffset y reutilizarlo:

 @interface CustomLayout () @property (nonatomic) NSValue *previousContentOffset; @end @implementation CustomLayout - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset { CGPoint previousContentOffset = [self.previousContentOffset CGPointValue]; CGPoint superContentOffset = [super targetContentOffsetForProposedContentOffset:proposedContentOffset]; return self.previousContentOffset != nil ? previousContentOffset : superContentOffset ; } - (void)prepareForTransitionFromLayout:(UICollectionViewLayout *)oldLayout { self.previousContentOffset = [NSValue valueWithCGPoint:self.collectionView.contentOffset]; return [super prepareForTransitionFromLayout:oldLayout]; } - (void)finalizeLayoutTransition { self.previousContentOffset = nil; return [super finalizeLayoutTransition]; } @end 

Todo lo que hace es guardar el desplazamiento de contenido anterior antes de la transición de diseño en prepareForTransitionFromLayout , sobrescribiendo el nuevo desplazamiento de contenido en targetContentOffsetForProposedContentOffset , y targetContentOffsetForProposedContentOffset en finalizeLayoutTransition . Muy claro

Si ayuda a agregar al cuerpo de la experiencia: encontré este problema persistentemente independientemente del tamaño de mi contenido, si configuré un recuadro de contenido o cualquier otro factor obvio. Entonces mi solución fue algo drástica. Primero clasifiqué UICollectionView y agregué para combatir la configuración de compensación de contenido inapropiado:

 - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated { if(_declineContentOffset) return; [super setContentOffset:contentOffset]; } - (void)setContentOffset:(CGPoint)contentOffset { if(_declineContentOffset) return; [super setContentOffset:contentOffset]; } - (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated { _declineContentOffset ++; [super setCollectionViewLayout:layout animated:animated]; _declineContentOffset --; } - (void)setContentSize:(CGSize)contentSize { _declineContentOffset ++; [super setContentSize:contentSize]; _declineContentOffset --; } 

No me siento orgulloso de ello, pero la única solución factible parece ser rechazar por completo cualquier bash de la vista de recostackción de establecer su propia compensación de contenido como resultado de una llamada a setCollectionViewLayout:animated: Empíricamente, parece que este cambio se produce directamente en la llamada inmediata, que obviamente no está garantizada por la interfaz o la documentación, pero tiene sentido desde el punto de vista de Core Animation, así que tal vez solo me siento un 50% incómodo con la suposición.

Sin embargo, hubo un segundo problema: UICollectionView ahora agregaba un pequeño salto a las vistas que permanecían en el mismo lugar en un nuevo diseño de vista de colección, empujándolas hacia abajo unos 240 puntos y luego animándolas a la posición original. No tengo claro por qué, pero modifiqué mi código para tratarlo, sin embargo, cortando las CAAnimation que se habían agregado a las celdas que, de hecho, no se movían:

 - (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated { // collect up the positions of all existing subviews NSMutableDictionary *positionsByViews = [NSMutableDictionary dictionary]; for(UIView *view in [self subviews]) { positionsByViews[[NSValue valueWithNonretainedObject:view]] = [NSValue valueWithCGPoint:[[view layer] position]]; } // apply the new layout, declining to allow the content offset to change _declineContentOffset ++; [super setCollectionViewLayout:layout animated:animated]; _declineContentOffset --; // run through the subviews again... for(UIView *view in [self subviews]) { // if UIKit has inexplicably applied animations to these views to move them back to where // they were in the first place, remove those animations CABasicAnimation *positionAnimation = (CABasicAnimation *)[[view layer] animationForKey:@"position"]; NSValue *sourceValue = positionsByViews[[NSValue valueWithNonretainedObject:view]]; if([positionAnimation isKindOfClass:[CABasicAnimation class]] && sourceValue) { NSValue *targetValue = [NSValue valueWithCGPoint:[[view layer] position]]; if([targetValue isEqualToValue:sourceValue]) [[view layer] removeAnimationForKey:@"position"]; } } } 

Esto parece no nhibernate las vistas que realmente se mueven, o hacer que se muevan incorrectamente (como si esperaran que todo a su alrededor se redujera 240 puntos y animar a la posición correcta con ellas).

Entonces esta es mi solución actual.

Probablemente he pasado aproximadamente dos semanas tratando de obtener varios diseños para la transición entre ellos sin problemas. Descubrí que la anulación propuesta está funcionando en iOS 10.2, pero en la versión anterior todavía tengo el problema. Lo que empeora mi situación es que necesito hacer una transición a otro diseño como resultado de un desplazamiento, por lo que la vista se desplaza y realiza la transición al mismo tiempo.

La respuesta de Tommy fue lo único que funcionó para mí en versiones anteriores a 10.2. Estoy haciendo lo siguiente ahora.

 class HackedCollectionView: UICollectionView { var ignoreContentOffsetChanges = false override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) { guard ignoreContentOffsetChanges == false else { return } super.setContentOffset(contentOffset, animated: animated) } override var contentOffset: CGPoint { get { return super.contentOffset } set { guard ignoreContentOffsetChanges == false else { return } super.contentOffset = newValue } } override func setCollectionViewLayout(_ layout: UICollectionViewLayout, animated: Bool) { guard ignoreContentOffsetChanges == false else { return } super.setCollectionViewLayout(layout, animated: animated) } override var contentSize: CGSize { get { return super.contentSize } set { guard ignoreContentOffsetChanges == false else { return } super.contentSize = newValue } } } 

Luego, cuando configuro el diseño, hago esto …

 let theContentOffsetIActuallyWant = CGPoint(x: 0, y: 100) UIView.animate(withDuration: animationDuration, delay: 0, options: animationOptions, animations: { collectionView.setCollectionViewLayout(layout, animated: true, completion: { completed in // I'm also doing something in my layout, but this may be redundant now layout.overriddenContentOffset = nil }) collectionView.ignoreContentOffsetChanges = true }, completion: { _ in collectionView.ignoreContentOffsetChanges = false collectionView.setContentOffset(theContentOffsetIActuallyWant, animated: false) }) 

Esto finalmente funcionó para mí (Swift 3)

 self.collectionView.collectionViewLayout = UICollectionViewFlowLayout() self.collectionView.setContentOffset(CGPoint(x: 0, y: -118), animated: true)