La layout dynamic sur UICollectionView entraîne une modification inexistante du contenu

Selon la documentation d'Apple (et vanté à la WWDC 2012), il est possible de définir dynamicment la disposition sur UICollectionView et même d'animer les changements:

Vous spécifiez normalement un object physique lors de la création d'une vue de collection, mais vous pouvez également modifier la disposition d'une vue de collection de manière dynamic. L'object de layout est stocké dans la propriété collectionViewLayout. La définition de cette propriété met directement à jour la layout immédiatement, sans animer les modifications. Si vous souhaitez animer les modifications, vous devez appeler la méthode setCollectionViewLayout: animated: à la place.

Cependant, en pratique, j'ai trouvé que UICollectionView rend inexplicable et même des modifications non valides dans contentOffset , provoquant le déplacement incorrect des cellules, ce qui rend la fonctionnalité pratiquement inutilisable. Pour illustrer le problème, j'ai mis en place l'exemple de code suivant qui peut être attaché à un controller de vue de collection par défaut déposé dans un storyboard:

 #import <UIKit/UIKit.h> @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 

Le controller définit un UICollectionViewFlowLayout par défaut dans viewDidLoad et affiche une seule cellule à l'écran. Lorsque les cellules sont sélectionnées, le controller crée un autre UICollectionViewFlowLayout par défaut et le définit dans la vue de collection avec l'indicateur animated:YES . Le comportement attendu est que la cellule ne bouge pas. Le comportement réel, cependant, est que la cellule défile hors écran, à quel point il n'est même pas possible de faire défiler la cellule à l'écran.

Regarder le journal de la console révèle que le contentOffset a inexplicablement changé (dans mon projet, de (0, 0) à (0, 205)). J'ai posté une solution pour la solution pour le cas non animé ( ie animated:NO ), mais comme j'ai besoin d'animation, je suis très intéressé de savoir si quelqu'un a une solution ou une solution de contournement pour le cas animé.

En note, j'ai testé des mises en page personnalisées et obtenu le même comportement.

Je me suis débarrassé de mes cheveux pendant des jours et j'ai trouvé une solution à ma situation qui peut aider. Dans mon cas, j'ai une layout photo effondrement comme dans l'application photos sur l'ipad. Il affiche les albums avec les photos les unes sur les autres et lorsque vous appuyez sur un album, il agrandit les photos. Donc ce que j'ai est deux UICollectionViewLayouts séparés et je suis en train de basculer entre eux avec [self.collectionView setCollectionViewLayout:myLayout animated:YES] J'avais votre problème exact avec les cellules qui sautaient avant l'animation et contentOffset réalisé que c'était contentOffset . J'ai tout essayé avec le contentOffset mais il a quand même sauté pendant l'animation. La solution de Tyler ci-dessus a fonctionné mais elle était encore en train de jouer avec l'animation.

Puis j'ai remarqué que cela n'arrive que lorsqu'il y avait quelques albums sur l'écran, pas assez pour remplir l'écran. Ma disposition est -(CGSize)collectionViewContentSize comme recommandé. Lorsqu'il n'y a que quelques albums, la taille du contenu de la vue de la collection est inférieure à la taille du contenu de la vue. Cela provoque le saut lorsque je bascule entre les dispositions de la collection.

J'ai donc défini une propriété sur mes mises en page appelée minHeight et la définir sur la hauteur des parents de la vue des collections. Ensuite, je vérifie la hauteur avant de revenir dans -(CGSize)collectionViewContentSize Je m'assure que la hauteur est> = la hauteur minimale.

Pas une vraie solution mais ça marche bien maintenant. J'essaierais de définir la contentSize de contentSize de votre vue de collection pour être au less la longueur de la vue contenant.

edit: Manicaesar a ajouté une solution de contournement facile si vous héritez 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 contient la méthode targetContentOffsetForProposedContentOffset: qui vous permet de fournir le décalage de contenu approprié lors d'un changement de disposition, et cela s'anime correctement. Ceci est disponible dans iOS 7.0 et au-dessus

Ce problème m'a également mordu et il semble que ce soit un bug dans le code de transition. De ce que je peux dire, il essaie de se concentrer sur la cellule qui était la plus proche du centre de la disposition de la vue avant la transition. Cependant, s'il ne se trouve pas de cellule au centre de la pré-transition de la vue, alors elle essaie toujours de centrer l'endroit où la cellule serait post-transition. Ceci est très clair si vous définissez alwaysBounceVertical / Horizontal sur YES, chargez la vue avec une seule cellule et effectuez ensuite une transition de disposition.

J'ai été capable de contourner cela en explicitement dire à la collection de se concentrer sur une cellule spécifique (la première cellule visible de la cellule, dans cet exemple) après avoir déclenché la mise à jour de la layout.

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

Sauter avec une réponse tardive à ma propre question.

La bibliothèque TLLayoutTransitioning fournit une excellente solution à ce problème en réatsortingbuant les API de transition interactives iOS7 pour effectuer des transitions de layout non interactives. Il fournit effectivement une alternative à setCollectionViewLayout , résolvant le problème de décalage de contenu et ajoutant plusieurs fonctionnalités:

  1. Durée de l'animation
  2. 30+ courbes d' accélération (avec la permission de la bibliothèque AHEasing de Warren Moore)
  3. Modes de décalage de contenu multiple

Les courbes d' AHEasingFunction personnalisées peuvent être définies en tant AHEasingFunction fonctions AHEasingFunction . Le décalage de contenu final peut être spécifié en termes d'un ou plusieurs paths d'index avec les options de placement Minimal, Center, Top, Left, Bottom ou Right.

Pour voir ce que je veux dire, essayez d'exécuter la démo de redimensionnement dans l'espace de travail Exemples et de jouer avec les options.

L'utilisation est comme ça. D'abord, configurez votre controller de vue pour renvoyer une instance de TLTransitionLayout :

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

Ensuite, au lieu d'appeler setCollectionViewLayout , appelez transitionToCollectionViewLayout:toLayout défini dans la catégorie 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]; 

Cet appel initie une transition interactive et, en interne, un callback CADisplayLink qui entraîne la progression de la transition avec la durée et la fonction d' CADisplayLink spécifiées.

L'étape suivante consiste à spécifier un décalage de contenu final. Vous pouvez spécifier n'importe quelle valeur arbitraire, mais la méthode toContentOffsetForLayout définie dans UICollectionView-TLLayoutTransitioning fournit un moyen élégant de calculer les décalages de contenu relatifs à un ou plusieurs paths d'index. Par exemple, pour qu'une cellule spécifique se termine le plus près possible du centre de la vue de collection, effectuez l'appel suivant immédiatement après 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; 

Facile.

Animez votre nouvelle layout et contentOffset de collectionView dans le même bloc d'animation.

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

Cela conservera la constante self.collectionView.contentOffset .

Si vous searchz simplement que le décalage de contenu ne change pas lors de la transition entre les mises en page, vous pouvez créer une layout personnalisée et replace plusieurs methods pour garder trace de l'ancien object contentOffset et le réutiliser:

 @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 

Tout ce qui est en train de faire est d'save le décalage de contenu précédent avant la transition de layout dans prepareForTransitionFromLayout , en écrasant le nouveau décalage de contenu dans targetContentOffsetForProposedContentOffset , et en l' targetContentOffsetForProposedContentOffset dans targetContentOffsetForProposedContentOffset . Assez simple

rapide 3 .

Sous- class UICollectionViewLayout . La façon dont cdemiris99 a suggéré:

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

Je l'ai testé moi-même en utilisant ma propre layout personnalisée

Si cela aide à append de l'expérience, j'ai rencontré ce problème de façon persistante, indépendamment de la taille de mon contenu, que j'avais défini un encart de contenu ou tout autre facteur évident. Donc, ma solution de contournement était quelque peu drastique. D'abord, j'ai sous-classé UICollectionView et ajouté pour lutter contre le paramètre de décalage de contenu inapproprié:

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

Je n'en suis pas fier mais la seule solution réalisable semble être de rejeter complètement toute tentative de la vue de collection pour définir son propre offset de contenu résultant d'un appel à setCollectionViewLayout:animated: Empiriquement, il semble que ce changement se produise directement dans l'appel immédiat, ce qui n'est évidemment pas garanti par l'interface ou la documentation, mais est sensé d'un sharepoint vue Core Animation, je ne suis peut-être que 50% mal à l'aise.

Cependant, il y avait un deuxième problème: UICollectionView ajoutait maintenant un petit saut à ces vues qui restaient au même endroit sur une nouvelle disposition de vue de collection – les abaissant d'environ 240 points et les animant à la position originale. Je ne comprends pas pourquoi, mais j'ai modifié mon code pour y faire face en coupant les CAAnimation qui avaient été ajoutés à toutes les cellules qui, en fait, ne CAAnimation pas:

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

Cela ne semble pas inhiber les vues qui bougent réellement, ou les faire bouger incorrectement (comme si elles s'attendaient à ce que tout ce qui les entoure soit en baisse d'environ 240 points et à s'animer à la bonne position avec elles).

Donc, ceci est ma solution actuelle.

J'ai probablement passé environ deux semaines maintenant à essayer d'get une layout variée pour une transition en douceur. J'ai trouvé que replace l'offset proposé fonctionne dans iOS 10.2, mais dans la version antérieure à cela, j'ai toujours le problème. Ce qui rend ma situation un peu pire, c'est que je dois passer à une autre layout à la suite d'un défilement, de sorte que la vue défile et se déplace en même time.

La réponse de Tommy était la seule chose qui a fonctionné pour moi dans les versions antérieures à 10.2. Je fais la chose suivante maintenant.

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

Puis, quand je définis la layout, je fais ça …

 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) }) 

Cela a finalement fonctionné pour moi (Swift 3)

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