Uso de Contextos de Invalidación para UICollectionViewLayout


Así que he implementado encabezados pegajosos de trabajo en mi UICollectionView en parte devolviendo YES desde shouldInvalidateLayoutForBoundsChange:. Sin embargo, esto afecta el rendimiento y no quiero invalidar todo el diseño, solo mi sección de encabezado.

Ahora, de acuerdo con la documentación oficial, puedo usar UICollectionViewLayoutInvalidationContext para definir un contexto de invalidación personalizado para mi diseño, pero la documentación es muy deficiente. Me pide que " defina propiedades personalizadas que representen las partes de sus datos de diseño que se pueden volver a calcular independientemente", pero no entiendo lo que quieren decir con esto.

¿Alguien tiene alguna experiencia en subclase UICollectionViewLayoutInvalidationContext?

Author: Benjohn, 2013-11-20

5 answers

Esto es para iOS8

Experimenté un poco y creo que descubrí la forma limpia de usar el diseño de invalidación, al menos hasta que Apple amplíe un poco la documentación.

El problema que estaba tratando de resolver era conseguir encabezados pegajosos en la vista de colección. Tenía código de trabajo para esto usando la subclase de FlowLayout y overriding layoutAttributesForElementsInRect: (puedes encontrar ejemplos de trabajo en Google). Esto me requirió siempre volver verdadero de Shouldinvalidatelayout Forboundschange: que es el supuesto gran golpe de rendimiento en las tuercas que Apple quiere que evitemos con invalidación contextual.

La Invalidación del Contexto Limpio

Solo necesita subclasificar el UICollectionViewFlowLayout. No necesitaba una subclase para UICollectionViewLayoutInvalidationContext, pero entonces este podría ser un caso de uso bastante sencillo.

A medida que la vista de colección se desplaza, el diseño de flujo comenzará a recibir shouldInvalidateLayoutForBoundsChange: las llamadas. Dado que flow layout ya puede manejar esto, devolveremos la respuesta de la superclase al final de la función. Con un simple desplazamiento, esto será falso, y no reordenará los elementos. Pero necesitamos reorganizar los encabezados y hacer que permanezcan en la parte superior de la pantalla, por lo que le diremos a la vista de colección que invalide solo el contexto que proporcionaremos:

override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
    invalidateLayoutWithContext(invalidationContextForBoundsChange(newBounds))
    return super.shouldInvalidateLayoutForBoundsChange(newBounds)
}

Esto significa que necesitamos sobreescribir la función invalidationContextForBoundsChange: demasiado. Dado que el funcionamiento interno de esta función es desconocido, solo le pediremos a la superclase el objeto de contexto de invalidación, determinaremos qué elementos de la vista de colección queremos invalidar y agregaremos esos elementos al contexto de invalidación. Tomé parte del código para centrarme en lo esencial aquí:

override func invalidationContextForBoundsChange(newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext! {

    var context = super.invalidationContextForBoundsChange(newBounds)

    if /... we find a header in newBounds that needs to be invalidated .../ {

            context.invalidateSupplementaryElementsOfKind(UICollectionElementKindSectionHeader, atIndexPaths:[NSIndexPath(forItem: 0, inSection:headerIndexPath.section)] )
    }
    return context
}

Eso es todo. El encabezado y nada más que el encabezado se invalida. El diseño de flujo recibirá solo una llamada a layoutAttributesForSupplementaryViewOfKind: con el indexPath en el contexto de la invalidación. Si necesita invalidar celdas o decoradores, hay otras funciones invalidate* en el UICollectionViewLayoutInvalidationContext.

La parte más difícil realmente es determinar los indexPaths de las cabeceras en la función invalidationContextForBoundsChange:. Tanto mis encabezados como mis celdas tienen un tamaño dinámico y se necesitaron algunas acrobacias para que funcionara con solo mirar los límites CGRect, ya que la función más obviamente útil, indexPathForItemAtPoint:, no devuelve nada si el punto está en un encabezado, pie de página, decorador o espaciado de filas.

En cuanto al rendimiento, no hice una medición completa, pero un vistazo rápido a Time Profiler mientras se desplaza muestra que está haciendo algo bien (el pico más pequeño a la derecha está mientras se desplaza). UICollectionViewLayoutInvalidationContext performance comparison

 28
Author: meelawsh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-11-08 06:31:18

Yo estaba haciendo la misma pregunta sobre esto hoy y también me confundí con la parte: "defina propiedades personalizadas que representen las partes de sus datos de diseño que se pueden volver a calcular de forma independiente"

Lo que hice fue subclase UICollectionViewLayoutInvalidationContext (llamémosla CustomInvalidationContext) y añadí mi propia propiedad. Para fines de prueba, quería averiguar dónde podía configurar y recuperar estas propiedades desde el contexto, así que simplemente agregué una matriz como una propiedad llamada "atributos".

Entonces en mi subclases UICollectionViewLayout I sobrescribir +invalidationContextClass devolver una instancia de CustomInvalidationContext que se devuelve en otro método que sobrescribir que es: -invalidationContextForBoundsChange. En este método, debe llamar a super, que devuelve una instancia de CustomInvalidationContext, que luego configura las propiedades y devuelve. Establezco la matriz de atributos para que tenga objetos @["a","b","c"];

Esto se recupera más tarde en otro método sobrescrito -invalidateLayoutWithContext:. Pude recuperar los atributos que establecí del contexto pasado.

So lo que puede hacer es establecer propiedades que más tarde le permitirán calcular qué indexPaths se suministrarán a -layoutAttributesForElementsInRect:.

Espero que ayude.

 6
Author: Peeks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-13 21:17:07

A partir de iOS 8

Esta respuesta fue escrita antes de la semilla de iOS 8. Menciona la funcionalidad esperada que no existía en iOS 7 y ofrece una solución. Esta funcionalidad ahora existe y funciona. Otra respuesta, actualmente más abajo en la página, describe un enfoque para iOS 8.


Discusión

Primero – con cualquier tipo de optimización, la precaución realmente importante es perfilar primero y comprender dónde exactamente su rendimiento cuello de botella es.

He mirado UICollectionViewLayoutInvalidationContext y estoy de acuerdo en que parece que podría proporcionar las características necesarias. En los comentarios sobre la pregunta describí mis intentos de hacer que esto funcione. Ahora sospecho que, si bien le permite eliminar los cálculos de diseño, no le ayudará a evitar hacer cambios de diseño en las celdas de contenido. En mi caso, los cálculos de diseño no son especialmente costosos, pero quiero evitar que el marco aplique cambios de diseño a las celdas de desplazamiento simples (de las cuales tengo un buen número), y solo aplicarlos a las celdas "especiales".

Resumen de la aplicación

A la luz de no hacerlo como parecía su intención de Apple, he engañado. Yo uso 2 UICollectionView instancias. Tengo el contenido de desplazamiento normal en una vista de fondo, y los encabezados en una vista de segundo plano. Los diseños de las vistas especifican que la vista de fondo no invalida el cambio de límites, y la vista de primer plano sí.

Aplicación detalles

Hay una serie de cosas no obvias que necesitas hacer bien para que esto funcione, y también tengo algunos consejos para la implementación que encontré que me hicieron la vida más fácil. Revisaré esto y proporcionaré fragmentos de código tomados de mi solicitud. No voy a dar una solución completa aquí, pero voy a dar todas las piezas que necesita.

UICollectionView tiene una propiedad backgroundView.

Creo la vista de fondo en mi UICollectionViewController's viewDidLoad método. Desde este punto de vista el controlador ya tiene una instancia UICollectionView en su propiedad collectionView. Esta va a ser la vista de primer plano y se utilizará para elementos con un comportamiento de desplazamiento especial, como la fijación.

Creo una segunda instancia UICollectionView y la establezco como la propiedad backgroundView de la vista de colección en primer plano. Configuré el fondo para usar también la subclase UICollectionViewController como fuente de datos y delegado. Deshabilito la interacción del usuario en la vista de fondo porque de lo contrario parece obtener todos los eventos. Usted podría requiere un comportamiento más sutil que este si desea selecciones, etc:

…
UICollectionView *const foregroundCollectionView = [self collectionView];
UICollectionView *const backgroundCollectionView = [[UICollectionView alloc] initWithFrame: [foregroundCollectionView frame] collectionViewLayout: [[STGridLayout alloc] init]];
[backgroundCollectionView setDataSource: self];
[backgroundCollectionView setDelegate: self];
[backgroundCollectionView setUserInteractionEnabled: NO];
[foregroundCollectionView setBackgroundView: backgroundCollectionView];
[(STGridLayout*)[backgroundCollectionView collectionViewLayout] setInvalidateLayoutForBoundsChange: NO];
[(STGridLayout*)[foregroundCollectionView collectionViewLayout] setInvalidateLayoutForBoundsChange: YES];
…

En resumen – en este punto tenemos dos vistas de colección una encima de la otra. El de atrás se utilizará para el contenido estático. La delantera será para el contenido fijado y tal. Ambos están apuntando al mismo UICollectionViewController como su delegado y fuente de datos.

La propiedad invalidateLayoutForBoundsChange en STGridLayout es algo que he añadido a mi diseño personalizado. El layout simplemente lo devuelve cuando -(BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds es called.

Entonces hay más conjunto común a ambos puntos de vista que en mi caso se ve así:

for(UICollectionView *collectionView in @[foregroundCollectionView, backgroundCollectionView])
{
  // Configure reusable views.
  [STCollectionViewStaticCVCell registerForReuseInView: collectionView];
  [STBlockRenderedCollectionViewCell registerForReuseInView: collectionView];
}

El método registerForReuseInView: es algo añadido a UICollectionReusableView por una categoría, junto con dequeueFromView:. El código para estos está al final de la respuesta.

La siguiente pieza para ir a viewDidLoad es el único dolor de cabeza importante con este enfoque.

Cuando arrastra la vista de primer plano, necesita la vista de fondo para desplazarse con ella. Voy a mostrar el código para esto en un momento: simplemente se refleja la vista de primer plano es contentOffset a la vista de fondo. Sin embargo, probablemente querrás que las vistas de desplazamiento puedan "rebotar" en los bordes del contenido. Parece que UICollectionViewsujetará el contentOffset cuando se establece programáticamente, de modo que el contenido no se desprenda de los límites de UICollectionView. Sin un remedio, solo los elementos pegajosos del primer plano rebotarán, lo que se ve horrible. Sin embargo, agregar lo siguiente a su viewDidLoad solucionará esto:

CGSize size = [foregroundCollectionView bounds].size;
[backgroundCollectionView setContentInset: UIEdgeInsetsMake(size.width, size.height, size.width, size.height)];

Desafortunadamente, esto fix significa que cuando la vista aparece en la pantalla el desplazamiento de contenido del fondo no coincidirá con el primer plano. Para arreglar esto necesitarás implementar esto:

-(void) viewDidAppear:(BOOL)animated
{
  [super viewDidAppear: animated];
  UICollectionView *const foregroundCollectionView = [self collectionView];
  UICollectionView *const backgroundCollectionView = (UICollectionView *)[foregroundCollectionView backgroundView];
  [backgroundCollectionView setContentOffset: [foregroundCollectionView contentOffset]];
}

Estoy seguro de que tendría más sentido hacer esto en viewDidAppear:, pero eso no funcionó para mí.

La última cosa importante que necesita es mantener el desplazamiento del fondo sincronizado con el primer plano de esta manera:

-(void) scrollViewDidScroll:(UIScrollView *const)scrollView
{
  UICollectionView *const collectionView = [self collectionView];
  if(scrollView == collectionView)
  {
    const CGPoint contentOffset = [collectionView contentOffset];
    UIScrollView *const backgroundView = (UIScrollView*)[collectionView backgroundView];
    [backgroundView setContentOffset: contentOffset];
  }
}

Consejos de implementación

Estas son algunas sugerencias que me han ayudado a implementar UICollectionViewController ' s data source methods.

En primer lugar, he utilizado una sección para cada uno de los diferentes tipos de vistas que se superponen. Esto funcionó bien para mí. No he usado las vistas suplementarias o de decoración de UICollectionView. Le doy a cada sección un nombre en una enumeración al comienzo de mi controlador de vista como este:

enum STSectionNumbers
{
  number_the_first_section_0_even_if_they_are_moved_during_editing = -1,

  // Section names. Order implies z with earlier sections drawn behind latter sections.
  STBackgroundCellsSection,
  STDataCellSection,
  STDayHeaderSection,
  STColumnHeaderSection,

  // The number of sections.
  STSectionCount,
};

En mi subclase UICollectionViewLayout, cuando se piden atributos de diseño, configuro la propiedad z para cumplir con el orden de esta manera:

-(UICollectionViewLayoutAttributes*) layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
  UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath: indexPath];
  const CGRect frame = …
  [attributes setFrame: frame];
  [attributes setZIndex: [indexPath section]];
  return attributes;
}

Para mí, la lógica de la fuente de datos es mucho más simple si doy ambos de las instancias UICollectionView todas de las secciones, pero controlo qué vista realmente las obtiene haciendo que las secciones estén vacías para la otra.

Aquí hay un método práctico que puedo usar para verificar si un determinado UICollectionView realmente tiene un número de secciones en particular:

-(BOOL) collectionView:(UICollectionView *const)collectionView hasSection:(const NSUInteger)section
{
  const BOOL isForegroundView = collectionView == [self collectionView];
  const BOOL isBackgroundView = !isForegroundView;

  switch (section)
  {
    case STBackgroundCellsSection:
    case STDataCellSection:
    {
      return isBackgroundView;
    }

    case STColumnHeaderSection:
    case STDayHeaderSection:
    {
      return isForegroundView;
    }

    default:
    {
      return NO;
    }
  }
}

Con esto en su lugar, es realmente fácil escribir los métodos de origen de datos. Ambas vistas tienen el mismo número de secciones, como he dicho, así que:

-(NSInteger) numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
  return STSectionCount;
}

Sin embargo, tienen recuentos de células diferentes en las secciones, pero esto es fácil de acomodar

-(NSInteger) collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
  if(![self collectionView: collectionView hasSection: section])
  {
    return 0;
  }

  switch(section)
  {
    case STDataCellSection:
    {
      return … // (actual logic not shown)
    }

    case STBackgroundCellsSection:
    {
      return …
    }

    … // similarly for other sections.

    default:
    {
      return 0;
    }
  }
}

Mi subclase UICollectionViewLayout también tiene algunos métodos dependientes de la vista que delega a la subclase UICollectionViewController, pero estos se manejan fácilmente usando el patrón anterior:

-(NSArray*) collectionViewRowRanges:(UICollectionView *)collectionView inSection:(NSInteger)section
{
  if(![self collectionView: collectionView hasSection: section])
  {
    return [NSArray array];
  }

  switch(section)
  {
    case STDataCellSection:
    {
      return … // (actual logic omitted)
      }
    }

    case STBackgroundCellsSection:
    {
      return …
    }

    … // etc for other sections

    default:
    {
      return [NSArray array];
    }
  }
}

Como una comprobación de cordura, me aseguro de que las vistas de colección solo soliciten celdas de las secciones que deberían mostrar:

-(UICollectionViewCell*) collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
  assert([self collectionView: collectionView hasSection: [indexPath section]] && "Check views are only asking for the sections they own.");

  switch([indexPath section])
  {
    case STBackgroundCellsSection:
    … // You get the idea.

Finalmente, vale la pena señalar que como se muestra en otra SA respuesta las matemáticas para las secciones adhesivas son más simples de lo que imaginaba, siempre y cuando pienses que todo (incluida la pantalla del dispositivo) se encuentra en el espacio de contenido de la vista de colección.

Código para UICollectionReusableView Categoría de reutilización

@interface UICollectionReusableView (Reuse)
+(void) registerForReuseInView: (UICollectionView*) view;
+(id) dequeueFromView: (UICollectionView*) view withIndexPath: (NSIndexPath *) indexPath;
@end

Su implementación es:

@implementation UICollectionReusableView (Reuse)
+(void) registerForReuseInView: (UICollectionView*) view
{
  [view registerClass: self forCellWithReuseIdentifier: NSStringFromClass(self)];
}

+(instancetype) dequeueFromView: (UICollectionView*) view withIndexPath: (NSIndexPath *) indexPath
{
  return [view dequeueReusableCellWithReuseIdentifier:NSStringFromClass(self) forIndexPath: indexPath];
}
@end
 3
Author: Benjohn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:31:15

Implementé exactamente lo que está tratando de hacer estableciendo una bandera que me dice por qué se llama a Preparerlayout, y luego solo recalculando la posición de las celdas adhesivas.

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    _invalidatedBecauseOfBoundsChange = YES;
    return YES;
}

Entonces en prepareLayout lo hago:

if (!_invalidatedBecauseOfBoundsChange)
{
    [self calculateStickyCellsPositions];
}
_invalidateBecauseOfBoundsChange = NO;
 1
Author: Fabien Warniez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-04 00:09:56

Estoy trabajando en la subclase UICollectionViewLayout personalizada. Traté de usar UICollectionViewLayoutInvalidationContext. Cuando actualizo layout / view que no necesita toda la UICollectionViewLayout para recalcular todos sus atributos, utilizo mi subclase UICollectionViewLayoutInvalidationContext en invalidateLayoutWithContext: y en prepareLayout recalculo solo los atributos especificados en mis propiedades de subclase UICollectionViewLayoutInvalidationContext en lugar de recalcular todos los atributos.

 0
Author: Pitiphong Phongpattranont,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-16 17:37:00