Il y a 10 ans -
Temps de lecture 6 minutes
À la (re)découverte de la UICollectionView
Vous avez peut-être déjà travaillé avec la classe UICollectionView, mais savez-vous comment personnaliser entièrement la mise en page et les transitions de vos données ? De même, avez-vous déjà eu à mettre en œuvre un UICollectionViewLayout personnalisé ?
Si la réponse est "non", alors une lecture de cette article s’impose.
Parmi les nouveaux composants UI introduits par iOS 6, un des plus appréciés est la UICollectionView. Nous verrons ici comment se servir de cet objet en réalisant une galerie photo animée.
La UICollectionView est un composant qui gère une collection ordonnée d’éléments et les présente au travers de vues complètement personnalisables.
Par exemple, il est possible de présenter des données sous forme de cellules dans une grille, un carrousel ou bien encore dans une vue rotative. La CollectionView obtient ses données d’un dataSource qui se conforme au protocole UICollectionViewDataSource. Elle les affiche via des classes de type UICollectionViewCell.
Un protocole UICollectionViewDelegate est mis à disposition par UIKit afin d’interagir avec la CollectionView. Il permet notamment d’identifier les éléments sélectionnés.
La UICollectionView, de façon similaire à la UITableView, est une sous-classe de la UIScrollView, rajoutant un dataSource, des événements (sélection, désélection, etc) et un layout. Si nous n’utilisons que de simples collectionViews au travers de Storyboards pour réaliser nos interfaces, l’usage du layout peut passer inaperçu. En réalité, il constitue un des principaux atouts de ce composant car il est conçu pour encapsuler toute la logique d’affichage des éléments.
Notre objectif est d’obtenir une galerie dans laquelle les images suivantes et précédentes seront semi-transparentes et sur lesquelles un redimensionnement sera appliqué. La taille de l’image augmentera au fur et à mesure qu’elle s’approchera du centre de l’écran. L’image suivante présente le résultat souhaité:
Création du UICollectionView
Note: Si nous devions cibler la version 5.0 d’iOS (qui ne supporte pas les UICollectionViews), nous pourrions utiliser la très bonne librairie PSTCollectionView, réalisée par Peter Steinberger, qui réplique parfaitement le comportement de l’UICollectionView.
La mise en place d’une UICollectionView n’a rien de complexe : il suffit d’instancier ce composant à travers un storyboard ou bien directement via le code. Chaque image de la galerie sera contenue dans une cellule de type XBGalleryCell (Une sous-classe de UICollectionViewCell). Par exemple :
XBGalleryLayout *layout = [[XBGalleryLayout alloc] init]; self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 320, 320) collectionViewLayout:layout]; self.collectionView.dataSource = self; self.collectionView.delegate = self; [self.collectionView registerClass:[XBGalleryCell class] forCellWithReuseIdentifier:@"GalleryCell"];
Le code ci-dessus crée une UICollectionView avec une taille arbitraire, un layout de type XBGalleryLayout. Un dataSource et delegate sont également définis. La méthode registerClass permet enfin d’attribuer la classe de la cellule à un reuseIdentifier spécifique, ce qui permet à UIKit d’allouer puis réutiliser un objet de classe “XBGalleryCell” pour chaque cellule ayant reuseIdentifier égal à “GalleryCell”.
Pour obtenir le nombre d’éléments de la CollectionView, nous mettons en place les méthodes suivantes :
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { ... } - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { ... }
De même, pour obtenir la cellule associée à un élément de la source de données, nous utilisons le code suivant :
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { static NSString *cellIdentifier = @"GalleryCell"; UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath]; [cell setImageWithURL:[self.imageUrls[indexPath.row] url] placeholderImage:self.placeholderImage]; return cell; }
UICollectionViewCell
Nous continuons avec la UICollectionViewCell qui, de la même façon que la UITableViewCell, présente le contenu d’un élément de la dataSource :
@implementation XBGalleryCell - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.imageView = [[UIImageView alloc] initWithFrame:self.bounds]; self.imageView.contentMode = UIViewContentModeScaleAspectFit; [self addSubview:self.imageView]; } return self; } - (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholderImage { [self.imageView setImageWithURL:url placeholderImage:placeholderImage]; } @end
UICollectionViewLayout
Le principal atout de la UICollectionView est le UICollectionViewLayout. Cette classe permet de configurer l’aspect et les animations des items contenues dans notre UICollectionView. UICollectionViewLayout est une classe abstraite qui doit nécessairement être sous-classée et qui contient toutes les informations sur la mise en page d’une UICollectionView.
Grâce au UICollectionViewLayout, nous pouvons appliquer les transitions, les effets d’opacité et de redimensionnement des images que nous avons décrits en début d’article.
Ci-dessous l’implémentation utilisée pour XBGalleryLayout (le code complet est disponible sur gist à cette URL https://gist.github.com/viteinfinite/5735887) :
@implementation XBGalleryLayout - (id)init { self = [super init]; if (self) { // Taille des cellules self.itemSize = CGSizeMake(imageWitdh, imageHeight); // Configuration de l'aspect self.minimumLineSpacing = -10; // Espace minimum entre les cellules self.sectionInset = UIEdgeInsetsMake(0, imageLeftMargin, 0, imageLeftMargin); // "Padding" // Direction de défilement self.scrollDirection = UICollectionViewScrollDirectionHorizontal; } return self; } ... @end
Modifier l’aspect et l’animation des cellules
Grâce à la méthode layoutAttributesForElementsInRect il est possible de modifier l’aspect (layout attribute) des éléments affichés à l’intérieur d’une région de l’écran, et notamment de modifier l’opacité et la taille selon la position d’une cellule à l’écran.
Un objet de type UICollectionViewLayoutAttributes permet de configurer les propriétés suivantes :
- frame
- center
- size
- transform3D
- alpha
- zIndex
- hidden
L’implémentation de cette méthode dans notre XBGalleryLayout est la suivante :
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect { NSArray *layoutAttributes = [super layoutAttributesForElementsInRect:rect]; CGFloat horizontalCenter = (CGRectGetWidth(self.collectionView.bounds) / 2.0f); [layoutAttributes enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *layoutAttributes, NSUInteger idx, BOOL *stop) { CGPoint centerInCollectionView = layoutAttributes.center; CGPoint centerInMainView = [self.collectionView.superview convertPoint:centerInCollectionView fromView:self.collectionView]; if (centerInMainView.x > -80 && centerInMainView.x < self.collectionView.frame.size.width + 80){ CGFloat translateYBy = [self translateYByOffset:centerInMainView.x fromCenter:horizontalCenter]; CGFloat scaleBy = [self scaleByOffset:centerInMainView.x fromCenter:horizontalCenter]; CGFloat alpha = [self alphaByOffset:centerInMainView.x fromCenter:horizontalCenter]; CATransform3D transform = CATransform3DIdentity; transform = CATransform3DScale(transform, scaleBy, scaleBy, 1.0); transform = CATransform3DTranslate(transform, 0.0, translateYBy, 0.0); layoutAttributes.transform3D = transform; layoutAttributes.alpha = alpha; } }]; return layoutAttributes; }
Un UICollectionViewLayout permet également de contrôler lorsque la collectionView doit arrêter le défilement inertiel. Ce contrôle est rendu possible grâce à la méthode targetContentOffsetForProposedContentOffset:withScrollingVelocity: qui nous donne la possibilité de modifier le content offset (c’est à dire le décalage du contenu) à partir du contentOffset initialement prévu et de la vélocité de défilement. En d’autres termes, il est possible de forcer la collectionView à réduire l’inertie afin de positionner l’image courante au centre de l’écran :
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity { CGFloat offsetAdjustment = MAXFLOAT; CGFloat horizontalOffset = proposedContentOffset.x + imageLeftMargin; CGRect targetRect = CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height); NSArray *array = [super layoutAttributesForElementsInRect:targetRect]; for (UICollectionViewLayoutAttributes *layoutAttributes in array) { CGFloat itemOffset = layoutAttributes.frame.origin.x; if (fabsf(itemOffset - horizontalOffset) < fabsf(offsetAdjustment)) { offsetAdjustment = itemOffset - horizontalOffset; } } return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y); }
Conclusion
La UICollectionView est un élément extrêmement flexible qui permet de s’affranchir de nombreuses complexités, et qui permet notamment une personnalisation en profondeur de la présentation d’une collection d’éléments.
Ce qui rend le composant UICollectionView particulièrement intéressant est la façon dont il est architecturé. Grâce au UICollectionViewLayout, les responsabilités sont partagées efficacement entre les classes. Cela permet une séparation plus propre entre la logique du contrôleur et celle de la vue et donc une meilleure maintenabilité du code.
Commentaire