uicollectionview custom layout span columns and Sticky Header with animation.
UICollectionView is highly customizable component introduced in IOS. It’s behaviour can be customized easily with Custom Layout attached to it. As much as its customizable, its hard to control everything in it. It requires high understanding of its inner working or we have to do lot’s of try and solve error to control item animation while reloading particular item.
There is lot’s of different blogs avilable to study it, though it seems no blog making it really easy to understand. I was facing same problem in one of my project and we had to support Column Span of cell with custom animation. Also I was forced to support Sticky Header to it just like UITableViewHeader.
Here I would like to share my experience with Custom Layout of UICollectionView, considering it will be helpful to some one.
Firstly UICollectionView has been created as per the document using FlowLayout.
It was looking something like below:
So this simple Grid has 5 cells in image1 and while its did selected in image2 its 4th cell has been expanded to two column by giving size in sizeForItemAtIndexPath delegage method of CollectionView. But problem here is in second row. Its showing only one cell while ideally we needed two cell in that row. Also its animation should be perfect while expanding/Collepsing. Othe thing we need to point out here is, if cell will be of odd number it will work fine without doing anything. So we have to just customize layout if cell is of even number considering cell count starts from 1.
Span Columns Support in Collection View
Now our first main aim is to fill the gape FlowLayout missing. Let’s take Onc class inhariting UICollectionViewFlowLayout
@interface ColumnSpanFlowLayout : UICollectionViewFlowLayout
here I am listing few key methods in which we have to do changes in order to achive our goal:
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath: (NSIndexPath *)indexPath
this method will be called for getting attribute of individual cell.
- (CGSize)collectionViewContentSize
this method will be called when it will require to finalize the size of the view. We have to calculate here and need to send exactly required size. Counting is as per the actull required row. For example in image1 we needed only 4 rows exactly so 4 * 120 height is required. For this calculation I have made method as below:
-(int)getActualRowForSection:(int)section { int countRows= [self.collectionView:numberOfItemsInSection:section]; int actualRows = ceil(countRows/2.0); if(indexPathSelected.section == section) { if(countRows % 2 == 0) { actualRows = actualRows + 2; } else { actualRows = actualRows + 1; } } return actualRows; }
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect { rect.origin.x = rect.origin.x - 120; rect.size.height = rect.size.height + 240; NSMutableArray * attributes = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
this method is called when view requires to update visible cells only. In this method we applied one hack to get few cells beyond the screen as above mentioned. Its just not to keep any space unfilled if there should be any space.
Other logic is bit lengthy to mention here, developers can check in code directly its pretty straight forward if one can go step by step.
For Sticky Header:
There is one property defined to on/off it isHeaderStiky.
Frankly, I have taken this code from some of stack overflow posts, its code will lie in two methods as below
–
-(UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:[indexPath retain]]; if([kind isEqualToString:UICollectionElementKindSectionHeader]) { attributes = [self getModifiedAttributeForSection:attributes]; } if(isHeaderStiky) { if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { UICollectionView * const cv = self.collectionView; CGPoint const contentOffset = cv.contentOffset; CGPoint nextHeaderOrigin = CGPointMake(INFINITY, INFINITY); if (indexPath.section+1 < [cv numberOfSections]) { UICollectionViewLayoutAttributes *nextHeaderAttributes = [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:[NSIndexPath indexPathForItem:0 inSection:indexPath.section+1]]; nextHeaderOrigin = nextHeaderAttributes.frame.origin; } CGRect frame = attributes.frame; if (self.scrollDirection == UICollectionViewScrollDirectionVertical) { frame.origin.y = MIN(MAX(contentOffset.y, frame.origin.y), nextHeaderOrigin.y - CGRectGetHeight(frame)); } else { // UICollectionViewScrollDirectionHorizontal frame.origin.x = MIN(MAX(contentOffset.x, frame.origin.x), nextHeaderOrigin.x - CGRectGetWidth(frame)); } attributes.zIndex = 1024; attributes.frame = frame; } } return attributes; } - (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect { rect.origin.x = rect.origin.x - 120; rect.size.height = rect.size.height + 240; NSMutableArray * attributes = [[super layoutAttributesForElementsInRect:rect] mutableCopy]; [attributes enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) { if (attributes.representedElementCategory == UICollectionElementCategoryCell) { [self getModifiedAttributeForRow:attributes]; } else if (attributes.representedElementCategory == UICollectionElementCategorySupplementaryView) { [self getModifiedAttributeForSection:attributes]; } }]; if(isHeaderStiky) { NSMutableIndexSet *missingSections = [NSMutableIndexSet indexSet]; for (NSUInteger idx=0; idx<[attributes count]; idx++) { UICollectionViewLayoutAttributes *layoutAttributes = attributes[idx]; if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) { [missingSections addIndex:layoutAttributes.indexPath.section]; // remember that we need to layout header for this section } if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) { [attributes removeObjectAtIndex:idx]; // remove layout of header done by our super, we will do it right later idx--; } } // layout all headers needed for the rect using self code [missingSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx]; UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath]; if (layoutAttributes != nil) { [attributes addObject:layoutAttributes]; } }]; } return attributes; }
for sticky header we must implement below method:
- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBound { return YES; }
For Animation Handling:
- (UICollectionViewLayoutAttributes*) initialLayoutAttributesForAppearingItemAtIndexPath: (NSIndexPath *)itemIndexPath
this is only method I used to control animation, there is other three methods which we can use as per our requrement.
- (UICollectionViewLayoutAttributes*) finalLayoutAttributesForDisappearingItemAtIndexPath: (NSIndexPath *)itemIndexPath - (UICollectionViewLayoutAttributes*) initialLayoutAttributesForAppearingSupplementaryElementOfKind: (NSString *)elementKind atIndexPath: (NSIndexPath *)elementIndexPath - (UICollectionViewLayoutAttributes*) finalLayoutAttributesForDisappearingSupplementaryElementOfKind: (NSString *)elementKind atIndexPath: (NSIndexPath *)elementIndexPath
Real Implementation in UICollectionView Custom Layout:
Create two properties:
NSIndexPath *indexPathSelected; NSIndexPath *indexPathLastSel;
Creation of CustomLayout and UICollectionView:
layout = [[UICollectionViewFlowLayout alloc] init]; layout.minimumInteritemSpacing = 0.f; layout.minimumLineSpacing = 0.f; layout.scrollDirection = UICollectionViewScrollDirectionVertical; layout.itemSize = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? CGSizeMake(120, 120) : CGSizeMake(160, 120); UICollectionView * collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];
code in didSelect should be something as below:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { [collectionView deselectItemAtIndexPath:indexPath animated:YES]; indexPathLastSel = indexPathSelected; layout.indexPathLastSel = indexPathSelected; if([indexPathSelected isEqual:indexPath]) { indexPathSelected = nil; layout.indexPathSelected = nil; } else { indexPathSelected = indexPath; layout.indexPathSelected = indexPath; } [self.collectionView performBatchUpdates:^{ [collectionView reloadItemsAtIndexPaths: [NSArray arrayWithObject:indexPath]]; } completion:^(BOOL finished) { //[self.collectionView reloadData]; }]; }
please return double of the size as below:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { if([indexPath isEqual:indexPathSelected]) { return CGSizeMake(320, 240); } return CGSizeMake(160, 120); }
that’s it. here is github link to download working example source code. Happy coding.