web-dev-qa-db-fra.com

UICollectionView flowLayout n'encapsule pas correctement les cellules

J'ai un UICollectionView avec un FLowLayout. Cela fonctionnera comme je m'y attend la plupart du temps, mais de temps en temps l'une des cellules ne s'enroule pas correctement. Par exemple, la cellule qui devrait être activée dans la première "colonne" de la troisième ligne si elle se trouve en fait à la fin de la deuxième ligne et il y a juste un espace vide où elle devrait être (voir le diagramme ci-dessous). Tout ce que vous pouvez voir de cette cellule rouge est le côté gauche (le reste est coupé) et l'endroit où elle devrait être est vide.

Cela ne se produit pas systématiquement; ce n'est pas toujours la même ligne. Une fois que c'est arrivé, je peux faire défiler vers le haut puis vers l'arrière et la cellule se sera réparée. Ou, lorsque j'appuie sur la cellule (qui m'amène à la vue suivante via un Push) puis que je reviens en arrière, je vois la cellule dans la position incorrecte, puis elle saute à la position correcte.

La vitesse de défilement semble faciliter la reproduction du problème. Lorsque je défile lentement, je peux toujours voir la cellule dans la mauvaise position de temps en temps, mais elle sautera immédiatement à la bonne position.

Le problème a commencé lorsque j'ai ajouté les encarts des sections. Auparavant, j'avais les cellules presque alignées contre les limites de la collection (peu ou pas d'encarts) et je n'ai pas remarqué le problème. Mais cela signifiait que la droite et la gauche de la vue de la collection étaient vides. C'est à dire, ne pouvait pas défiler. De plus, la barre de défilement n'était pas alignée à droite.

Je peux faire en sorte que le problème se produise à la fois sur le simulateur et sur un iPad 3.

Je suppose que le problème se produit en raison des insertions des sections gauche et droite ... Mais si la valeur est incorrecte, je m'attendrais à ce que le comportement soit cohérent. Je me demande si cela pourrait être un bug avec Apple? Ou peut-être que cela est dû à une accumulation d'encarts ou quelque chose de similaire.

Illustration of problem and settings


Suivi : J'utilise cette réponse ci-dessous par Nick depuis plus de 2 ans maintenant sans problème (au cas où les gens sont me demandant s'il y a des trous dans cette réponse - je n'en ai pas encore trouvé). Bravo Nick.

77
lindon fox

Il existe un bogue dans l'implémentation de layoutAttributesForElementsInRect par UICollectionViewFlowLayout qui lui fait renvoyer DEUX objets d'attribut pour une seule cellule dans certains cas impliquant des insertions de section. L'un des objets attribut renvoyés n'est pas valide (en dehors des limites de la vue de collection) et l'autre est valide. Vous trouverez ci-dessous une sous-classe de UICollectionViewFlowLayout qui résout le problème en excluant les cellules en dehors des limites de la vue de collection.

// 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

Voir ceci .

D'autres réponses suggèrent de retourner OUI à partir de shouldInvalidateLayoutForBoundsChange, mais cela provoque des recalculs inutiles et ne résout même pas complètement le problème.

Ma solution résout complètement le bogue et ne devrait pas poser de problème lorsque Apple corrige la cause racine.

92
Nick Snyder

j'ai découvert des problèmes similaires dans mon application iPhone. La recherche sur le forum Apple dev m'a apporté cette solution appropriée qui a fonctionné dans mon cas et le sera probablement aussi dans votre cas:

Sous-classe UICollectionViewFlowLayout et remplacez shouldInvalidateLayoutForBoundsChange pour retourner YES.

//.h
@interface MainLayout : UICollectionViewFlowLayout
@end

et

//.m
#import "MainLayout.h"
@implementation MainLayout
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    return YES;
}
@end
7
xxtesaxx

Mettez ceci dans le viewController qui possède la vue de collection

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    [self.collectionView.collectionViewLayout invalidateLayout];
}
7
Peter Lapisu

A Swift version de la réponse 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 }
    }
}
7
Patrick Pijnappel

J'ai également eu ce problème pour une mise en page de base avec des encarts pour les marges. Le débogage limité que j'ai fait pour l'instant implémente - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect dans ma sous-classe UICollectionViewFlowLayout et en enregistrant ce que l'implémentation de super classe renvoie, ce qui montre clairement le problème.

- (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;
}

En implémentant - (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath je peux également voir qu'il semble renvoyer les mauvaises valeurs pour itemIndexPath.item == 30, qui est le facteur 10 du nombre de cellules par ligne de ma grille, je ne sais pas si c'est pertinent.

- (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;
}

Avec un manque de temps pour plus de débogage, la solution de contournement que j'ai faite pour l'instant est de réduire la largeur de ma collectionviews avec un montant égal à la marge gauche et droite. J'ai un en-tête qui a toujours besoin de toute la largeur, j'ai donc défini clipsToBounds = NO dans ma collectionview, puis j'ai également supprimé les encarts gauche et droit, semble fonctionner. Pour que la vue d'en-tête reste ensuite en place, vous devez implémenter le décalage et le dimensionnement des trames dans les méthodes de mise en page qui sont chargées de renvoyer layoutAttributes pour la vue d'en-tête.

5
monowerker

J'ai ajouté un rapport de bogue à Apple. Ce qui fonctionne pour moi, c'est de définir la section inférieureInset à une valeur inférieure à l'encart supérieur.

4
DrMickeyLauer

Je viens de rencontrer un problème similaire avec des cellules disparaissant après le défilement UICollectionView sur iOS 1 (aucun problème sur iOS 6-9).

Sous-classement de UICollectionViewFlowLayout et substitution de la méthode layoutAttributesForElementsInRect: ne fonctionne pas dans mon cas.

La solution était assez simple. Actuellement, j'utilise une instance de UICollectionViewFlowLayout et je mets itemSize et estimeItemSize (je n'ai pas utilisé estiméItemSize auparavant) et je lui donne une taille non nulle. La taille réelle est calculée dans la collectionView: layout: sizeForItemAtIndexPath: method.

De plus, j'ai supprimé un appel de la méthode invalidateLayout de layoutSubviews afin d'éviter des rechargements inutiles.

3
Andrey Seredkin

Je rencontrais le même problème de déplacement de cellule sur l'iPhone en utilisant un UICollectionViewFlowLayout et j'étais donc heureux de trouver votre message. Je sais que vous rencontrez le problème sur un iPad, mais je le poste car je pense que c'est un problème général avec le UICollectionView. Voici donc ce que j'ai découvert.

Je peux confirmer que le sectionInset est pertinent pour ce problème. En plus de cela, le headerReferenceSize a également une influence sur le fait qu'une cellule soit déplacée ou non. (Cela a du sens car il est nécessaire pour calculer l'origine.)

Malheureusement, même des tailles d'écran différentes doivent être prises en compte. En jouant avec les valeurs de ces deux propriétés, j'ai constaté qu'une certaine configuration fonctionnait soit sur les deux (3,5 "et 4"), sur aucune ou sur une seule des tailles d'écran. Habituellement, aucun d'entre eux. (Cela a également un sens, puisque les limites du UICollectionView changent, donc je n'ai pas connu de disparité entre la rétine et la non-rétine.)

J'ai fini par définir les sectionInset et headerReferenceSize selon la taille de l'écran. J'ai essayé environ 50 combinaisons jusqu'à ce que je trouve des valeurs sous lesquelles le problème ne se reproduisait plus et la disposition était visuellement acceptable. Il est très difficile de trouver des valeurs qui fonctionnent sur les deux tailles d'écran.

Donc, en résumé, je peux simplement vous recommander de jouer avec les valeurs, de les vérifier sur différentes tailles d'écran et d'espérer que Apple corrigera ce problème.

3
Masa

Les réponses ci-dessus ne fonctionnent pas pour moi, mais après avoir téléchargé les images, j'ai remplacé

[self.yourCollectionView reloadData]

avec

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

pour rafraîchir et il peut afficher toutes les cellules correctement, vous pouvez l'essayer.

2
Yao Li

Je viens de rencontrer un problème similaire, mais j'ai trouvé une solution très différente.

J'utilise une implémentation personnalisée de UICollectionViewFlowLayout avec un défilement horizontal. Je crée également des emplacements de cadre personnalisés pour chaque cellule.

Le problème que je rencontrais était que [super layoutAttributesForElementsInRect: rect] ne renvoyait pas réellement tous les UICollectionViewLayoutAttributes qui devraient être affichés à l'écran. Lors des appels à [self.collectionView reloadData], certaines cellules seraient soudainement définies comme masquées.

Ce que j'ai fini par faire était de créer un NSMutableDictionary qui mettait en cache tous les UICollectionViewLayoutAttributes que j'ai vus jusqu'à présent, puis incluait tous les éléments que je savais devoir être affichés.

- (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;
}

Voici la catégorie pour NSMutableDictionary que les UICollectionViewLayoutAttributes sont enregistrés correctement.

#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
2
Endama

Cela peut être un peu tard, mais assurez-vous de définir vos attributs dans prepare() si possible.

Mon problème était que les cellules s'étalaient, puis obtenaient une mise à jour dans layoutAttributesForElements. Cela a entraîné un effet de scintillement lorsque de nouvelles cellules sont apparues.

En déplaçant toute la logique d'attribut dans prepare, puis en les définissant dans UICollectionViewCell.apply() cela a éliminé le scintillement et créé une cellule lisse de beurre affichant ????

0
Michael