UICollectionView flowLayout no envuelve las celdas correctamente

79

Tengo UICollectionViewun FLowLayout. Funcionará como espero la mayor parte del tiempo, pero de vez en cuando una de las celdas no se ajusta correctamente. Por ejemplo, la celda que debería estar en la primera "columna" de la tercera fila si en realidad está al final de la segunda fila y solo hay un espacio vacío donde debería estar (vea el diagrama a continuación). Todo lo que puede ver de esta celda roja es el lado izquierdo (el resto está cortado) y el lugar donde debería estar está vacío.

Esto no sucede de manera constante; no siempre es la misma fila. Una vez que ha sucedido, puedo desplazarme hacia arriba y luego hacia atrás y la celda se habrá arreglado sola. O, cuando presiono la celda (que me lleva a la siguiente vista presionando) y luego retrocedo, veré la celda en la posición incorrecta y luego saltará a la posición correcta.

La velocidad de desplazamiento parece facilitar la reproducción del problema. Cuando me desplazo lentamente, todavía puedo ver la celda en la posición incorrecta de vez en cuando, pero luego saltará a la posición correcta de inmediato.

El problema comenzó cuando agregué las inserciones de las secciones. Anteriormente, tenía las celdas casi alineadas con los límites de la colección (poco o ningún inserto) y no noté el problema. Pero esto significaba que las partes derecha e izquierda de la vista de colección estaban vacías. Es decir, no se pudo desplazar. Además, la barra de desplazamiento no estaba alineada a la derecha.

Puedo hacer que el problema ocurra tanto en Simulator como en un iPad 3.

Supongo que el problema se debe a las inserciones de las secciones izquierda y derecha ... Pero si el valor es incorrecto, entonces esperaría que el comportamiento fuera consistente. Me pregunto si esto podría ser un error de Apple. O quizás esto se deba a una acumulación de inserciones o algo similar.

Ilustración del problema y la configuración


Seguimiento : he estado usando esta respuesta a continuación por Nick durante más de 2 años sin ningún problema (en caso de que la gente se pregunte si hay algún agujero en esa respuesta, todavía no he encontrado ninguno). Bien hecho Nick.

lindon fox
fuente

Respuestas:

94

Hay un error en la implementación de layoutAttributesForElementsInRect de UICollectionViewFlowLayout que hace que devuelva DOS objetos de atributo para una sola celda en ciertos casos que involucran inserciones de sección. Uno de los objetos de atributo devueltos no es válido (fuera de los límites de la vista de colección) y el otro es válido. A continuación se muestra una subclase de UICollectionViewFlowLayout que soluciona el problema al excluir celdas fuera de los límites de la vista de colección.

// NDCollectionViewFlowLayout.h
@interface NDCollectionViewFlowLayout : UICollectionViewFlowLayout
@end

// NDCollectionViewFlowLayout.m
#import "NDCollectionViewFlowLayout.h"
@implementation NDCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
  NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
  NSMutableArray *newAttributes = [NSMutableArray arrayWithCapacity:attributes.count];
  for (UICollectionViewLayoutAttributes *attribute in attributes) {
    if ((attribute.frame.origin.x + attribute.frame.size.width <= self.collectionViewContentSize.width) &&
        (attribute.frame.origin.y + attribute.frame.size.height <= self.collectionViewContentSize.height)) {
      [newAttributes addObject:attribute];
    }
  }
  return newAttributes;
}
@end

Mira esto .

Otras respuestas sugieren devolver SÍ de shouldInvalidateLayoutForBoundsChange, pero esto causa recálculos innecesarios y ni siquiera resuelve por completo el problema.

Mi solución resuelve completamente el error y no debería causar ningún problema cuando Apple corrige la causa raíz.

Nick Snyder
fuente
5
Para su información, este error también surge con el desplazamiento horizontal. Reemplazar x con y y ancho con alto hace que este parche funcione.
Patrick Tescher
¡Gracias! Estaba empezando a jugar con collectionView (si quieres enviar spam a Apple sobre esto, aquí está la referencia de rdar openradar.appspot.com/12433891 )
Vinzzz
1
@richarddas No, no quieres comprobar si los rectos se cruzan. De hecho, todas las celdas (válidas o no válidas) se cruzarán con los límites de la vista de colección. Quiere comprobar si alguna parte del rect queda fuera de los límites, que es lo que hace mi código.
Nick Snyder
2
@Rpranata A partir de iOS 7.1, este error no se ha corregido. Suspiro.
finaliza el
2
Tal vez esté usando esto mal, pero en iOS 8.3 en Swift, esto está causando que las subvistas a la derecha que solían estar cortadas no aparezcan en absoluto. ¿Alguien mas?
sudo
8

Pon esto en el viewController que posee la vista de colección

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    [self.collectionView.collectionViewLayout invalidateLayout];
}
Peter Lapisu
fuente
¿Dónde pones eso?
fatuhoku
En el controlador de vista que posee la vista de colección
Peter Lapisu
3
Mi problema fue que las células desaparecieron por completo. Esta solución ayudó, sin embargo, esto provoca recargas innecesarias. Todavía está funcionando ahora .. ¡gracias!
pawi
1
provoca un bucle infinito si llamo desde viewController por mí
Hofi
Como señaló DHennessy13 , esta solución actual es buena pero puede ser imperfecta, ya que invalidará el diseño al girar la pantalla (y en la mayoría de los casos no debería). Una mejora podría ser colocar una bandera para que invalidateLayoutsolo una vez.
Cœur
7

Descubrí problemas similares en la aplicación de mi iPhone. Buscar en el foro de desarrollo de Apple me trajo esta solución adecuada que funcionó en mi caso y probablemente también en el suyo:

Subclase UICollectionViewFlowLayouty anular shouldInvalidateLayoutForBoundsChangepara regresar YES.

//.h
@interface MainLayout : UICollectionViewFlowLayout
@end

y

//.m
#import "MainLayout.h"
@implementation MainLayout
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    return YES;
}
@end
xxtesaxx
fuente
Esto solo resuelve parcialmente el problema que tengo con este problema. De hecho, la celda va al lugar correcto cuando aparece la fila. Sin embargo, justo antes de que aparezca, todavía aparece una celda a un lado.
Daniel Wood
Cuidado: hacer esto hará que el diseño se ejecute cada vez que se desplace. Esto puede afectar gravemente al rendimiento.
fatuhoku
7

Una versión rápida de la respuesta de Nick Snyder:

class NDCollectionViewFlowLayout : UICollectionViewFlowLayout {
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)
        let contentSize = collectionViewContentSize
        return attributes?.filter { $0.frame.maxX <= contentSize.width && $0.frame.maxY < contentSize.height }
    }
}
Patrick Pijnappel
fuente
1
esto hizo desaparecer completamente CollectionViewCell ... ¿Alguna otra solución posible?
Giggs
5

También he tenido este problema para un diseño de vista de cuadrícula básico con inserciones para márgenes. La depuración limitada que he hecho por ahora se está implementando - (NSArray *)layoutAttributesForElementsInRect:(CGRect)recten mi subclase UICollectionViewFlowLayout y registrando lo que devuelve la implementación de la superclase, lo que muestra claramente el problema.

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray *attrsList = [super layoutAttributesForElementsInRect:rect];

    for (UICollectionViewLayoutAttributes *attrs in attrsList) {
        NSLog(@"%f %f", attrs.frame.origin.x, attrs.frame.origin.y);
    }

    return attrsList;
}

Al implementar - (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath, también puedo ver que parece devolver los valores incorrectos para itemIndexPath.item == 30, que es el factor 10 de la cantidad de celdas por línea de mi vista de cuadrícula, no estoy seguro de si eso es relevante.

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
    UICollectionViewLayoutAttributes *attrs = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];

    NSLog(@"initialAttrs: %f %f atIndexPath: %d", attrs.frame.origin.x, attrs.frame.origin.y, itemIndexPath.item);

    return attrs;
}

Con la falta de tiempo para más depuración, la solución que he hecho por ahora es reducir el ancho de mi colección de vistas en una cantidad igual al margen izquierdo y derecho. Tengo un encabezado que todavía necesita el ancho completo, así que configuré clipsToBounds = NO en mi vista de colección y luego también eliminé las inserciones izquierda y derecha en él, parece funcionar. Para que la vista de encabezado permanezca en su lugar, debe implementar el cambio de marco y el tamaño en los métodos de diseño que tienen la tarea de devolver layoutAttributes para la vista de encabezado.

monowerker
fuente
Gracias por la información adicional @monowerker. Creo que mi problema comenzó cuando agregué las inserciones (agregué esto a la pregunta). Probaré sus métodos de depuración y veré si me dicen algo. Podría probar tu trabajo también.
lindon fox
Es muy probable que esto sea un error en UICFL / UICL, voy a intentar archivar un radar cuando tenga tiempo, aquí hay una discusión con algunos números de rdar a los que puede hacer referencia. twitter.com/steipete/status/258323913279410177
monowerker
4

He agregado un informe de error a Apple. Lo que me funciona es establecer la sección inferior Inset en un valor menor que la inserción superior.

DrMickeyLauer
fuente
3

Estaba experimentando el mismo problema de reemplazo de celda en el iPhone usando un UICollectionViewFlowLayout por lo que me alegré de encontrar tu publicación. Sé que tienes el problema en un iPad, pero estoy publicando esto porque creo que es un problema general con UICollectionView. Así que esto es lo que descubrí.

Puedo confirmar que el sectionInset es relevante para ese problema. Además de eso, headerReferenceSizetambién influye si una celda se desplaza o no. (Esto tiene sentido ya que es necesario para calcular el origen).

Desafortunadamente, incluso los diferentes tamaños de pantalla deben tenerse en cuenta. Al jugar con los valores de estas dos propiedades, experimenté que cierta configuración funcionaba en ambos (3,5 "y 4"), en ninguna o sólo en uno de los tamaños de pantalla. Generalmente ninguno de ellos. (Esto también tiene sentido, ya que los límites delUICollectionView cambios, por lo tanto, no experimenté ninguna disparidad entre retina y no retina).

Terminé configurando el sectionInsetyheaderReferenceSize dependiendo del tamaño de la pantalla. Probé alrededor de 50 combinaciones hasta que encontré valores por debajo de los cuales el problema ya no ocurría y el diseño era visualmente aceptable. Es muy difícil encontrar valores que funcionen en ambos tamaños de pantalla.

Entonces, resumiendo, solo puedo recomendarle que juegue con los valores, verifique estos en diferentes tamaños de pantalla y espero que Apple solucione este problema.

Masa
fuente
3

Acabo de encontrar un problema similar con las celdas que desaparecen después del desplazamiento de UICollectionView en iOS 10 (no tuve problemas en iOS 6-9).

Subclasificación de UICollectionViewFlowLayout y método de anulación layoutAttributesForElementsInRect: no funciona en mi caso.

La solución fue bastante simple. Actualmente utilizo una instancia de UICollectionViewFlowLayout y configuro tanto itemSize como estimadoItemSize (no estimadaItemSize antes) y lo configuré en un tamaño distinto de cero. El tamaño real se calcula en el método collectionView: layout: sizeForItemAtIndexPath:.

Además, eliminé una llamada al método invalidateLayout de layoutSubviews para evitar recargas innecesarias.

Andrey Seredkin
fuente
¿Dónde se estableció el tamaño del elemento y el tamaño estimado del elemento en el diseño de flujo de vista de colección de objetos?
Garrett Cox
UICollectionViewFlowLayout * flowLayout = [[UICollectionViewFlowLayout alloc] init]; [flowLayout setItemSize: CGSizeMake (200, 200)]; [flowLayout setEstimatedItemSize: CGSizeMake (200, 200)]; self.collectionView = [[UICollectionView alloc] initWithFrame: CGRectZero collectionViewLayout: flowLayout];
Andrey Seredkin
Establecer Estimación de
tamaño del artículo
2

Acabo de experimentar un problema similar pero encontré una solución muy diferente.

Estoy usando una implementación personalizada de UICollectionViewFlowLayout con un desplazamiento horizontal. También estoy creando ubicaciones de marcos personalizados para cada celda.

El problema que estaba teniendo era que [super layoutAttributesForElementsInRect: rect] en realidad no devolvía todos los UICollectionViewLayoutAttributes que deberían mostrarse en la pantalla. En las llamadas a [self.collectionView reloadData], algunas de las celdas de repente se establecerían como ocultas.

Lo que terminé haciendo fue crear un NSMutableDictionary que almacenó en caché todos los UICollectionViewLayoutAttributes que he visto hasta ahora y luego incluir los elementos que sé que deberían mostrarse.

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

    NSArray * originAttrs = [super layoutAttributesForElementsInRect:rect];
    NSMutableArray * attrs = [NSMutableArray array];
    CGSize calculatedSize = [self calculatedItemSize];

    [originAttrs enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * attr, NSUInteger idx, BOOL *stop) {
        NSIndexPath * idxPath = attr.indexPath;
        CGRect itemFrame = [self frameForItemAtIndexPath:idxPath];
        if (CGRectIntersectsRect(itemFrame, rect))
        {
            attr = [self layoutAttributesForItemAtIndexPath:idxPath];
            [self.savedAttributesDict addAttribute:attr];
        }
    }];

    // We have to do this because there is a bug in the collection view where it won't correctly return all of the on screen cells.
    [self.savedAttributesDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray * cachedAttributes, BOOL *stop) {

        CGFloat columnX = [key floatValue];
        CGFloat leftExtreme = columnX; // This is the left edge of the element (I'm using horizontal scrolling)
        CGFloat rightExtreme = columnX + calculatedSize.width; // This is the right edge of the element (I'm using horizontal scrolling)

        if (leftExtreme <= (rect.origin.x + rect.size.width) || rightExtreme >= rect.origin.x) {
            for (UICollectionViewLayoutAttributes * attr in cachedAttributes) {
                [attrs addObject:attr];
            }
        }
    }];

    return attrs;
}

Aquí está la categoría para NSMutableDictionary que los UICollectionViewLayoutAttributes se están guardando correctamente.

#import "NSMutableDictionary+CDBCollectionViewAttributesCache.h"

@implementation NSMutableDictionary (CDBCollectionViewAttributesCache)

- (void)addAttribute:(UICollectionViewLayoutAttributes*)attribute {

    NSString *key = [self keyForAttribute:attribute];

    if (key) {

        if (![self objectForKey:key]) {
            NSMutableArray *array = [NSMutableArray new];
            [array addObject:attribute];
            [self setObject:array forKey:key];
        } else {
            __block BOOL alreadyExists = NO;
            NSMutableArray *array = [self objectForKey:key];

            [array enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *existingAttr, NSUInteger idx, BOOL *stop) {
                if ([existingAttr.indexPath compare:attribute.indexPath] == NSOrderedSame) {
                    alreadyExists = YES;
                    *stop = YES;
                }
            }];

            if (!alreadyExists) {
                [array addObject:attribute];
            }
        }
    } else {
        DDLogError(@"%@", [CDKError errorWithMessage:[NSString stringWithFormat:@"Invalid UICollectionVeiwLayoutAttributes passed to category extension"] code:CDKErrorInvalidParams]);
    }
}

- (NSArray*)attributesForColumn:(NSUInteger)column {
    return [self objectForKey:[NSString stringWithFormat:@"%ld", column]];
}

- (void)removeAttributesForColumn:(NSUInteger)column {
    [self removeObjectForKey:[NSString stringWithFormat:@"%ld", column]];
}

- (NSString*)keyForAttribute:(UICollectionViewLayoutAttributes*)attribute {
    if (attribute) {
        NSInteger column = (NSInteger)attribute.frame.origin.x;
        return [NSString stringWithFormat:@"%ld", column];
    }

    return nil;
}

@end
Endama
fuente
También estoy usando el desplazamiento horizontal, me las arreglo para solucionar el problema con su solución, pero después de realizar la transición a otra vista y regresar, el tamaño del contenido parecía ser incorrecto cuando hay elementos adicionales que no se dividen en columnas por igual.
morph85
Encontré una solución para solucionar el problema por el cual la celda se ocultaba después de realizar la transición y volver a la vista de colección. Trate de no establecer EstimadoItemSize en collectionViewFlowLayout; establecer itemSize directamente.
morph85
2

Las respuestas anteriores no me funcionan, pero después de descargar las imágenes, reemplacé

[self.yourCollectionView reloadData]

con

[self.yourCollectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];

para actualizar y puede mostrar todas las celdas correctamente, puede intentarlo.

Yao Li
fuente
0

Esto puede ser un poco tarde, pero asegúrese de establecer sus atributos en prepare() si es posible.

Mi problema era que las celdas se estaban distribuyendo y luego se actualizaban layoutAttributesForElements. Esto resultó en un efecto de parpadeo cuando aparecieron nuevas celdas.

Al mover toda la lógica de atributos a prepare, luego establecerlos en UICollectionViewCell.apply()ella, se eliminó el parpadeo y se creó la visualización de celdas suaves de mantequilla 😊

Miguel
fuente