68 votes

Comment faire en sorte que les vues supplémentaires flottent dans UICollectionView comme le font les en-têtes de section dans UITableView en style simple ?

J'ai du mal à obtenir un effet d'"en-tête de section flottant" avec UICollectionView . Quelque chose qui a été assez facile dans UITableView (comportement par défaut pour UITableViewStylePlain ) semble impossible dans UICollectionView sans beaucoup de travail. Est-ce que je rate l'évidence ?

Apple ne fournit aucune documentation sur la manière d'y parvenir. Il semble que l'on doive sous-classer UICollectionViewLayout et mettre en place une mise en page personnalisée juste pour obtenir cet effet. Cela représente un travail considérable, avec la mise en œuvre des méthodes suivantes :

Méthodes à remplacer

Chaque objet de mise en page doit mettre en œuvre les méthodes suivantes :

collectionViewContentSize
layoutAttributesForElementsInRect:
layoutAttributesForItemAtIndexPath:
layoutAttributesForSupplementaryViewOfKind:atIndexPath: (if your layout supports supplementary views)
layoutAttributesForDecorationViewOfKind:atIndexPath: (if your layout supports decoration views)
shouldInvalidateLayoutForBoundsChange:

Cependant, je ne sais pas comment faire pour que la vue supplémentaire flotte au-dessus des cellules et "reste" en haut de la vue jusqu'à ce que la section suivante soit atteinte. Existe-t-il un indicateur à cet effet dans les attributs de mise en page ?

J'aurais utilisé UITableView mais j'ai besoin de créer une hiérarchie assez complexe de collections qui est facilement réalisable avec une vue de collection.

Tout conseil ou exemple de code serait grandement apprécié !

71voto

iPrabu Points 7216

Dans iOS9, Apple a eu la gentillesse d'ajouter une simple propriété dans le fichier UICollectionViewFlowLayout appelé sectionHeadersPinToVisibleBounds .

Avec cela, vous pouvez faire en sorte que les en-têtes flottent comme cela dans les vues de tableau.

let layout = UICollectionViewFlowLayout()
layout.sectionHeadersPinToVisibleBounds = true
layout.minimumInteritemSpacing = 1
layout.minimumLineSpacing = 1
super.init(collectionViewLayout: layout)

47voto

cocotutch Points 635

Soit mettre en œuvre les méthodes de délégation suivantes :

– collectionView:layout:sizeForItemAtIndexPath:
– collectionView:layout:insetForSectionAtIndex:
– collectionView:layout:minimumLineSpacingForSectionAtIndex:
– collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
– collectionView:layout:referenceSizeForHeaderInSection:
– collectionView:layout:referenceSizeForFooterInSection:

Dans votre contrôleur de vue qui a votre :cellForItemAtIndexPath (il suffit de renvoyer les valeurs correctes). Ou, au lieu d'utiliser les méthodes de délégation, vous pouvez également définir ces valeurs directement dans votre objet de mise en page, par ex. [layout setItemSize:size]; .

L'utilisation de l'une de ces méthodes vous permettra de définir vos paramètres dans le code plutôt que dans l'IB, car ils sont supprimés lorsque vous définissez une mise en page personnalisée. N'oubliez pas d'ajouter <UICollectionViewDelegateFlowLayout> à votre fichier .h, aussi !

Créez une nouvelle sous-classe de UICollectionViewFlowLayout appelez-le comme vous voulez, et assurez-vous que le fichier H a :

#import <UIKit/UIKit.h>

@interface YourSubclassNameHere : UICollectionViewFlowLayout

@end

Dans le fichier d'implémentation, assurez-vous qu'il contient les éléments suivants :

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

    NSMutableArray *answer = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
    UICollectionView * const cv = self.collectionView;
    CGPoint const contentOffset = cv.contentOffset;

    NSMutableIndexSet *missingSections = [NSMutableIndexSet indexSet];
    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
        if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
            [missingSections addIndex:layoutAttributes.indexPath.section];
        }
    }
    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
        if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
            [missingSections removeIndex:layoutAttributes.indexPath.section];
        }
    }

    [missingSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {

        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];

        UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];

        [answer addObject:layoutAttributes];

    }];

    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {

        if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {

            NSInteger section = layoutAttributes.indexPath.section;
            NSInteger numberOfItemsInSection = [cv numberOfItemsInSection:section];

            NSIndexPath *firstCellIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
            NSIndexPath *lastCellIndexPath = [NSIndexPath indexPathForItem:MAX(0, (numberOfItemsInSection - 1)) inSection:section];

            NSIndexPath *firstObjectIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
            NSIndexPath *lastObjectIndexPath = [NSIndexPath indexPathForItem:MAX(0, (numberOfItemsInSection - 1)) inSection:section];

            UICollectionViewLayoutAttributes *firstObjectAttrs;
            UICollectionViewLayoutAttributes *lastObjectAttrs;

            if (numberOfItemsInSection > 0) {
                firstObjectAttrs = [self layoutAttributesForItemAtIndexPath:firstObjectIndexPath];
                lastObjectAttrs = [self layoutAttributesForItemAtIndexPath:lastObjectIndexPath];
            } else {
                firstObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                                                                    atIndexPath:firstObjectIndexPath];
                lastObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter
                                                                   atIndexPath:lastObjectIndexPath];
            }

            CGFloat headerHeight = CGRectGetHeight(layoutAttributes.frame);
            CGPoint origin = layoutAttributes.frame.origin;
            origin.y = MIN(
                           MAX(
                               contentOffset.y + cv.contentInset.top,
                               (CGRectGetMinY(firstObjectAttrs.frame) - headerHeight)
                               ),
                           (CGRectGetMaxY(lastObjectAttrs.frame) - headerHeight)
                           );

            layoutAttributes.zIndex = 1024;
            layoutAttributes.frame = (CGRect){
                .origin = origin,
                .size = layoutAttributes.frame.size
            };

        }

    }

    return answer;

}

- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBound {

    return YES;

}

Choisissez "Custom" dans Interface Builder pour le Flow Layout, choisissez votre classe "YourSubclassNameHere" que vous venez de créer. Et c'est parti !

(Note : le code ci-dessus peut ne pas respecter les valeurs de contentInset.bottom, ou les objets de pied de page particulièrement grands ou petits, ou les collections qui ont 0 objets mais pas de pied de page).

3 votes

Si numberOfItemsInSection == 0 vous obtiendrez un EXC_ARITHMETIC code=EXC_I386_DIV (diviser par zéro ?) s'est écrasé sur cette ligne : UICollectionViewLayoutAttributes *firstCellAttrs = [self layoutAttributesForItemAtIndexPath:firstCellIndexPath]; . J'ai également remarqué que le contentOffset certaines parties de ce code ne respectent pas la valeur top-inset de la vue de la collection dans mon implémentation (peut être lié au problème précédent) Je proposerai un changement de code/une liste/etc.

3 votes

gist.github.com/toblerpwn/5393460 - n'hésitez pas à itérer, en particulier sur les limitations notées dans le gist :)

0 votes

Grande aide, merci. Cela a parfaitement fonctionné avec les classes de compatibilité de PSTCollectionView en renommant les classes concernées (UICollectionView/PSTCollectionView, ... etc.). github.com/steipete/PSTCollectionView

8voto

MikeV Points 506

Si vous avez une vue d'en-tête unique que vous souhaitez placer en haut de votre UICollectionView, voici un moyen relativement simple de le faire. Notez que cette méthode se veut aussi simple que possible. Elle suppose que vous utilisez un seul en-tête dans une seule section.

//Override UICollectionViewFlowLayout class
@interface FixedHeaderLayout : UICollectionViewFlowLayout
@end

@implementation FixedHeaderLayout
//Override shouldInvalidateLayoutForBoundsChange to require a layout update when we scroll 
- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}

//Override layoutAttributesForElementsInRect to provide layout attributes with a fixed origin for the header
- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect {

    NSMutableArray *result = [[super layoutAttributesForElementsInRect:rect] mutableCopy];

    //see if there's already a header attributes object in the results; if so, remove it
    NSArray *attrKinds = [result valueForKeyPath:@"representedElementKind"];
    NSUInteger headerIndex = [attrKinds indexOfObject:UICollectionElementKindSectionHeader];
    if (headerIndex != NSNotFound) {
        [result removeObjectAtIndex:headerIndex];
    }

    CGPoint const contentOffset = self.collectionView.contentOffset;
    CGSize headerSize = self.headerReferenceSize;

    //create new layout attributes for header
    UICollectionViewLayoutAttributes *newHeaderAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader withIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
    CGRect frame = CGRectMake(0, contentOffset.y, headerSize.width, headerSize.height);  //offset y by the amount scrolled
    newHeaderAttributes.frame = frame;
    newHeaderAttributes.zIndex = 1024;

    [result addObject:newHeaderAttributes];

    return result;
}
@end

Ver: https://gist.github.com/4613982

7voto

Mazyod Points 6072

Voici mon point de vue, je pense que c'est beaucoup plus simple que ce que j'ai pu voir ci-dessus. La principale source de simplicité est que je ne sous-classe pas la mise en page du flux, mais que je crée ma propre mise en page (beaucoup plus facile, si vous voulez mon avis).

Veuillez noter Je suppose que vous êtes déjà capable d'implémenter votre propre système personnalisé. UICollectionViewLayout qui affichera les cellules et les en-têtes sans implémentation flottante. Une fois que vous aurez écrit cette implémentation, alors seulement le code ci-dessous aura un sens. Encore une fois, c'est parce que l'OP demandait spécifiquement la partie des en-têtes flottants.

quelques bonus :

  1. Je fais flotter deux en-têtes, pas un seul.
  2. Les en-têtes poussent les en-têtes précédents hors du chemin
  3. Regarde, rapide !

note :

  1. supplementaryLayoutAttributes contient tous les attributs de l'en-tête sans mise en œuvre flottante
  2. J'utilise ce code dans prepareLayout puisque je fais tous les calculs en amont.
  3. n'oubliez pas de passer outre shouldInvalidateLayoutForBoundsChange à la vérité !

// float them headers
let yOffset = self.collectionView!.bounds.minY
let headersRect = CGRect(x: 0, y: yOffset, width: width, height: headersHeight)

var floatingAttributes = supplementaryLayoutAttributes.filter {
    $0.frame.minY < headersRect.maxY
}

// This is three, because I am floating 2 headers
// so 2 + 1 extra that will be pushed away
var index = 3
var floatingPoint = yOffset + dateHeaderHeight

while index-- > 0 && !floatingAttributes.isEmpty {

    let attribute = floatingAttributes.removeLast()
    attribute.frame.origin.y = max(floatingPoint, attribute.frame.origin.y)

    floatingPoint = attribute.frame.minY - dateHeaderHeight
}

5voto

Rocotilos Points 2341

Au cas où quelqu'un chercherait une solution en Objective-C, mettez ceci dans viewDidload :

    UICollectionViewFlowLayout *flowLayout = 
    (UICollectionViewFlowLayout*)_yourcollectionView.collectionViewLayout;
    [flowLayout setSectionHeadersPinToVisibleBounds:YES];

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X