web-dev-qa-db-fra.com

Utilisation de contextes d'invalidation pour UICollectionViewLayout

J'ai donc implémenté des en-têtes collants de travail dans mon UICollectionView en partie en renvoyant YES de shouldInvalidateLayoutForBoundsChange:. Cependant, cela a un impact sur les performances et je ne souhaite pas invalider la disposition entière, uniquement ma section d'en-tête.

Maintenant, selon la documentation officielle, je peux utiliser UICollectionViewLayoutInvalidationContext pour définir un contexte d'invalidation personnalisé pour ma mise en page, mais la documentation fait très défaut. Il me demande de "définir des propriétés personnalisées qui représentent les parties de vos données de mise en page qui peuvent être recalculées indépendamment", mais je ne comprends pas ce qu'elles signifient.

Quelqu'un at-il une expérience de sous-classement UICollectionViewLayoutInvalidationContext?

29
mattsson

C'est pour iOS8

J'ai expérimenté un peu et je pense avoir trouvé la façon propre d'utiliser la disposition d'invalidation, au moins jusqu'à ce que Apple développe un peu la documentation.

Le problème que j'essayais de résoudre était d'obtenir des en-têtes collants dans la vue de la collection. J'avais du code de travail pour cela en utilisant la sous-classe de FlowLayout et en remplaçant layoutAttributesForElementsInRect: (vous pouvez trouver des exemples de travail sur google). Cela m'a obligé à toujours renvoyer true de shouldInvalidateLayoutForBoundsChange: qui est le supposé coup de pouce majeur dans les noix que Apple veut que nous évitions avec une invalidation contextuelle.

L'invalidation du contexte propre

Il vous suffit de sous-classer UICollectionViewFlowLayout. Je n'avais pas besoin d'une sous-classe pour UICollectionViewLayoutInvalidationContext, mais cela pourrait être un cas d'utilisation assez simple.

Au fur et à mesure que la vue de la collection défile, la disposition du flux commence à recevoir des appels shouldInvalidateLayoutForBoundsChange :. Étant donné que la disposition du flux peut déjà gérer cela, nous retournerons la réponse de la superclasse à la fin de la fonction. Avec un simple défilement, cela sera faux et ne remodèlera pas les éléments. Mais nous devons réorganiser les en-têtes et les garder en haut de l'écran, nous dirons donc à la vue de collection d'invalider uniquement le contexte que nous fournirons:

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

Cela signifie que nous devons également remplacer la fonction invalidationContextForBoundsChange :. Étant donné que le fonctionnement interne de cette fonction est inconnu, nous allons simplement demander à la superclasse l'objet de contexte d'invalidation, déterminer quels éléments de vue de collection nous voulons invalider et ajouter ces éléments au contexte d'invalidation. J'ai sorti une partie du code pour me concentrer sur l'essentiel ici:

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
}

C'est tout. L'en-tête et rien que l'en-tête est invalidé. La disposition de flux ne recevra qu'un seul appel à layoutAttributesForSupplementaryViewOfKind: avec l'indexPath dans le contexte d'invalidation. Si vous aviez besoin d'invalider des cellules ou des décorateurs, il existe d'autres fonctions d'invalidation * sur le UICollectionViewLayoutInvalidationContext.

La partie la plus difficile est vraiment de déterminer les indexPaths des en-têtes dans la fonction invalidationContextForBoundsChange :. Mes en-têtes et mes cellules sont de taille dynamique et il a fallu quelques acrobaties pour le faire fonctionner en regardant simplement les limites CGRect, car la fonction la plus évidemment utile, indexPathForItemAtPoint:, ne renvoie rien si le point se trouve sur un en-tête, un pied de page, un décorateur ou écartement des rangs.

En ce qui concerne les performances, je n'ai pas fait de mesure complète, mais un rapide coup d'œil à Time Profiler pendant le défilement montre qu'il fait quelque chose de bien (le plus petit pic à droite est pendant le défilement). UICollectionViewLayoutInvalidationContext performance comparison

32
meelawsh

Je posais juste la même question à ce sujet aujourd'hui et je me suis également confondu avec la partie: "définir des propriétés personnalisées qui représentent les parties de vos données de disposition qui peuvent être recalculées indépendamment"

Ce que j'ai fait était la sous-classe UICollectionViewLayoutInvalidationContext (appelons-la CustomInvalidationContext) et j'ai ajouté ma propre propriété. À des fins de test, je voulais savoir où je pouvais configurer et récupérer ces propriétés à partir du contexte, j'ai donc simplement ajouté un tableau en tant que propriété appelée "attributs".

Ensuite, dans mon sous-classe UICollectionViewLayout j'écrase +invalidationContextClass pour renvoyer une instance de CustomInvalidationContext qui est retournée dans une autre méthode que j'écrase qui est: -invalidationContextForBoundsChange. Dans cette méthode, vous devez appeler super qui renvoie une instance de CustomInvalidationContext dont vous configurez ensuite les propriétés et retournez. J'ai défini le tableau d'attributs pour avoir des objets @["a","b","c"];

Ceci est ensuite récupéré plus tard dans une autre méthode écrasée -invalidateLayoutWithContext:. J'ai pu récupérer les attributs que j'ai définis à partir du contexte transmis.

Donc, ce que vous pouvez faire est de définir des propriétés qui vous permettront plus tard de calculer quels indexPaths doivent être fournis à -layoutAttributesForElementsInRect:.

J'espère que ça aide.

7
Peeks

Depuis iOS 8

Cette réponse a été écrite avant la graine iOS 8. Il mentionne les fonctionnalités espérées qui n'existaient pas tout à fait dans iOS 7 et propose une solution de contournement. Cette fonctionnalité existe maintenant et fonctionne. Une autre réponse, actuellement plus bas sur la page, décrit ne approche pour iOS 8 .


Discussion

Tout d'abord - avec tout type d'optimisation, la prudence la plus importante est de profiler d'abord et de comprendre où se situe exactement votre goulot d'étranglement des performances.

J'ai regardé UICollectionViewLayoutInvalidationContext et je suis d'accord pour dire qu'il pourrait fournir les fonctionnalités nécessaires. Dans les commentaires sur la question, j'ai décrit mes tentatives pour que cela fonctionne. Je soupçonne maintenant que même si cela vous permet de supprimer les recalculs de mise en page, cela ne vous aidera pas à éviter de modifier la mise en page des cellules de contenu. Dans mon cas, les calculs de mise en page ne sont pas particulièrement coûteux, mais je veux éviter que le framework applique des modifications de mise en page aux cellules de défilement simples (dont j'ai un certain nombre), et les applique uniquement aux cellules "spéciales".

Résumé de l'implémentation

À la lumière de ne pas le faire comme il semblait son intention par Apple, j'ai triché. J'utilise 2 UICollectionView instances. J'ai le contenu de défilement normal sur une vue d'arrière-plan et les en-têtes sur une deuxième vue de premier plan. Les dispositions des vues spécifient que la vue d'arrière-plan n'est pas invalidée en cas de modification des limites, et la vue de premier plan le fait.

Détails d'implémentation

Il y a un certain nombre de choses non évidentes dont vous avez besoin pour bien faire fonctionner, et j'ai également quelques conseils pour la mise en œuvre qui, selon moi, ont rendu ma vie plus facile. Je vais passer par là et fournir des extraits de code extraits de mon application. Je ne vais pas fournir une solution complète ici, mais je donnerai toutes les pièces dont vous avez besoin.

UICollectionView possède une propriété backgroundView.

Je crée la vue d'arrière-plan dans ma méthode UICollectionViewControllerviewDidLoad. À ce stade, le contrôleur de vue possède déjà une instance UICollectionView dans sa propriété collectionView. Ce sera la vue de premier plan et sera utilisée pour les éléments avec un comportement de défilement spécial tel que l'épinglage.

Je crée une deuxième instance UICollectionView et la définit comme la propriété backgroundView de la vue de la collection de premier plan. J'ai configuré l'arrière-plan pour utiliser également la sous-classe UICollectionViewController comme source de données et délégué. Je désactive l'interaction de l'utilisateur sur la vue d'arrière-plan car il semble autrement obtenir tous les événements. Vous pourriez avoir besoin d'un comportement plus subtil que cela si vous voulez des sélections, 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 résumé - à ce stade, nous avons deux vues de collection les unes sur les autres. L'arrière sera utilisé pour le contenu statique. Le premier sera pour le contenu épinglé et autres. Ils pointent tous les deux vers le même UICollectionViewController que leur délégué et leur source de données.

La propriété invalidateLayoutForBoundsChange sur STGridLayout est quelque chose que j'ai ajoutée à ma disposition personnalisée. La disposition la renvoie simplement lorsque -(BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds est appelée.

Il y a alors plus de configuration commune aux deux vues qui dans mon cas ressemble à ceci:

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

La méthode registerForReuseInView: Est quelque chose ajoutée à UICollectionReusableView par une catégorie, avec dequeueFromView:. Le code de ces derniers se trouve à la fin de la réponse.

La prochaine pièce à entrer dans viewDidLoad est le seul casse-tête majeur avec cette approche.

Lorsque vous faites glisser la vue de premier plan, vous avez besoin de la vue d'arrière-plan pour la faire défiler. Je vais montrer le code pour cela dans un instant: il reflète simplement le contentOffset de la vue de premier plan à la vue d'arrière-plan. Cependant, vous souhaiterez probablement que les vues de défilement puissent "rebondir" sur les bords du contenu. Il semble que UICollectionView sera clamp le contentOffset quand il est défini par programme, de sorte que le contenu ne se détache pas des limites du UICollectionView . Sans remède, seuls les éléments collants de premier plan rebondiront, ce qui a l'air horrible. Cependant, l'ajout de ce qui suit à votre viewDidLoad corrigera ce problème:

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

Malheureusement, ce correctif signifie que lorsque vous affichez apparaît à l'écran, le décalage de contenu de l'arrière-plan ne correspondra pas au premier plan. Pour résoudre ce problème, vous devez implémenter ceci:

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

Je suis sûr que cela aurait plus de sens de le faire dans viewDidAppear:, Mais cela n'a pas fonctionné pour moi.

La dernière chose importante dont vous avez besoin est de maintenir le défilement de l'arrière-plan synchronisé avec le premier plan comme ceci:

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

Conseils d'implémentation

Voici quelques suggestions qui m'ont aidé à implémenter les méthodes de source de données de UICollectionViewController.

Tout d'abord, j'ai utilisé une section pour chacun des différents types de vues superposées. Cela a bien fonctionné pour moi. Je n'ai pas utilisé les vues supplémentaires ou décoratives de UICollectionView. Je donne à chaque section un nom dans une énumération au début de mon contrôleur de vue comme ceci:

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

Dans ma sous-classe UICollectionViewLayout, lorsque des attributs de mise en page sont demandés, j'ai configuré la propriété z pour répondre à l'ordre comme ceci:

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

Pour moi, la logique de la source de données est beaucoup plus simple si je donne les deux des UICollectionView instances all = des sections, mais contrôlez quelle vue les obtient vraiment en rendant les sections vides pour l'autre.

Voici une méthode pratique que je peux utiliser pour vérifier si un UICollectionView vraiment a un numéro de section particulier:

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

Avec cela en place, il est vraiment facile d'écrire les méthodes de source de données. Les deux vues ont le même nombre de sections, comme je l'ai dit, donc:

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

Cependant, ils ont des nombres de cellules différents dans les sections, mais cela est facile à gérer

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

Ma sous-classe UICollectionViewLayout a également des méthodes dépendantes de la vue qu'elle délègue à la sous-classe UICollectionViewController, mais celles-ci sont facilement gérées en utilisant le modèle ci-dessus:

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

Pour vérifier l'intégrité, je m'assure que les vues de collection ne demandent que les cellules des sections qu'elles doivent afficher:

-(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.

Enfin, il convient de noter que, comme indiqué dans n autre SA réponse les mathématiques pour les sections collantes sont plus simples que je l'imaginais, à condition que vous pensiez à tout (y compris le l'écran de l'appareil) comme étant dans l'espace de contenu de la vue de la collection.

Code pour UICollectionReusableView Catégorie de réutilisation

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

Sa mise en œuvre est:

@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
4
Benjohn

J'ai implémenté exactement ce que vous essayez de faire en définissant un indicateur qui m'indique pourquoi prepareLayout est appelé, puis en recalculant uniquement la position des cellules collantes.

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

Puis dans prepareLayout je fais:

if (!_invalidatedBecauseOfBoundsChange)
{
    [self calculateStickyCellsPositions];
}
_invalidateBecauseOfBoundsChange = NO;
1
Fabien Warniez

Je travaille sur une sous-classe UICollectionViewLayout personnalisée. J'ai essayé d'utiliser UICollectionViewLayoutInvalidationContext. Lorsque je mets à jour la disposition/vue qui n'a pas besoin de la totalité de UICollectionViewLayout pour recalculer tous ses attributs, j'utilise ma sous-classe UICollectionViewLayoutInvalidationContext dans la invalidateLayoutWithContext: et dans prepareLayout je recalcule uniquement les attributs spécifiés dans mes propriétés de sous-classe UICollectionViewLayoutInvalidationContext au lieu de recalculer tous les attributs.