iOS Collection View Customization

As an app development company with quite a wide client base, we have to deal with a number of projects on a daily basis. The tasks we get differ in complexity and requirements. Dealing with different projects our developers sometimes elaborate their own solutions aimed at the best result possible. That’s why one is unlikely to give us an idea we can’t implement. This is pretty obvious for any company who puts quality on the main shelf. Here is an example of a programming issue our iOS developer solved.

Smells Like Excel Spirit

Not a long time ago I had to deal with one interesting project. The basic idea was to create something like Excel table, with fixed columns but in iPad. However, that would be quite an exaggeration to call it Excel, as being a type of a table for storing the data, my project excluded any sort of calculations.

The complexity was that in the data grid. It was supposed to be a table with multiple columns and rows. When scrolled horizontally, first few columns should have remained fixed and visible on the screen, letting all the other columns be sort of torn off and therefore easily scrolled in different directions. So basically it was supposed to look like a 2 component table, with one component fixed and the other one scrollable.

Looking for solutions

I started looking for solutions on the Internet. And here is what I found:

  1. iOS Data Grid Table View makes it easy to customize its layout. It claims to bring all the functionality you need to display the data in rows and columns. Sounds interesting, but looks expensive and, after all, who knows what it will eventually do to my table. So instead of taking the easy way I decided to keep looking and figure it out on my own.
  2. The next solution I came across was a Multi-Columns Table View with freeze column and header. It has a horizontal scrollable table body, a fixed left and top table header supporting foldable sections and draggable columns. The whole idea of this solution was to create 2 separate components (2 scrolls and 2 tables) synchronized in the vertical scrolling, which means pull one - the other component is dragged too. However, this implementation isn’t the best one for such sort of issues and would have caused many difficulties when changing the order of the columns, which I needed to be possible.
  3. Finally, I found some sort of inspiration or a direction I should look in, on Stackoverflow. The piece of the code here sets the first row/section as always visible and scrollable with the appropriate section/row. That’s where I got the main idea from! I had to add X coordinate of the contentOffset of the Collection View to the X coordinate of the cells that need to stay freeze. Why? Every time a user scrolls horizontally the cells from the fixed columns are moved along and stay visible on the screen.

Screenshot 2014-02-25 13.20.45

What to override in UICollectionView

I made up my mind to use UICollectionView with sections assigned for the columns and rows for the elements in the sections. There was a lot of work to do to adjust the collection to the needs of the project, so I allocated the main methods necessary to override:

- (CGSize)collectionViewContentSize. Obviously, as it’s a returning size of the collection’s content.

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect is the main method where all the magic happens.The input parameter is a rectangle, which is a visible part of the collection content. In order to return this method, an array of the objects UICollectionViewLayoutAttributes for every cell that gets into the indicated visible rectangle, comes into play. That’s where the calculations of the cell position and its offset, if needed, happens.

The decision was made to inherit UICollectionViewFlowLayout as unlike UICollectionViewLayout it invokes -layoutAttributesForElementsInRect: method any time a user scrolls through the collection. UICollectionViewFlowLayout allows either the horizontal or the vertical scrolling and I needed both options work simultaneously. That’s why I had to do the calculation of each cell position manually.

Content size issues

Another challenge I had to deal with was related to the size issues. The thing is all the rows and columns are different in height and width accordingly. How to calculate the exact size? Assign a delegate! That is all those values should be provided by a delegate object.

SAFreezedColumnsLayoutDelegate <NSObject>
- (CGFloat)widthForColumnAtIndex:(NSInteger)columnIndex;
- (CGFloat)heightForRowAtIndex:(NSInteger)columnIndex;

No surprise, a necessity to cache the received data would appear to avoid multiple recalculations of the same values. This way I could store the array of heights for each row and width for each column. In order to decrease the calculations, the arrays for X coordinates for all the columns and Y for all the rows were also stored like that.

At the start of the drawing process collectionView asks its collectionViewLayout object about the size of the content. How to give it the answer? Originally the idea was to upload all the data according to the size coordinates and those of the columns and rows at once. However, taking into account the volume of the data which can become really huge, I had to get rid of this idea and instead upload the data page by page. It looks like that:

Implemented class uploads the data for N pages. A user scrolls until there is M pages left (M < N surely). Then the next set of N pages gets uploaded and the cycle repeats. What does it mean? The method collectionViewContentSize returns a constantly increasing variable, which contains the current size of the collection content.

Obviously, I had to test and see if this solution is functional. So I created a prototype of a small table. My implementation of layoutAttributesForElementsInRect: method calculated and set attributes for all the cells in the collection as I wanted the test to be simple and fast. My prototype completed the task all right, but such an implementation couldn’t hold a bigger table. I had to limit the area of calculation of the coordinates to the visible cells only.

Visible rectangle area. How to define the cells?

There was a need to find a method that would define the exact cells that get into the given rectangle within the visible area. Taking into account a small number of the columns, I defined the ones destined to get into the visible rectangle by simple sort out: you should find a column, whose X coordinate is the closest but a smaller one to the leftmost point of the rectangle and relatively the column, whose X coordinate is the closest a bigger one to the rightmost point of the rectangle.

Unlike the columns, the number of the rows was significantly bigger, but there is an array for Y coordinates of the rows (in ascending order, apparently). I decided to use binary search method to find the number of the row with the closest Y coordinate. This method has a logarithmic complexity and therefore, shows good performance even with large data sets.


@protocol SAFreezedColumnsLayoutDelegate;
@interface PSFreezedColumnsLayout : UICollectionViewFlowLayout
@property (nonatomic, assign) NSInteger fixedColumnsCount;
@property (nonatomic, assign) NSInteger pagesBlockSize;
@property (nonatomic, assign) NSInteger pagesLeftToLoadBlock;
@property (nonatomic, weak) IBOutlet id<SAFreezedColumnsLayoutDelegate> delegate;
- (void)dropLayoutCache;
- (void)dropLayoutCacheFromRowIndex:(NSInteger)index;

@protocol SAFreezedColumnsLayoutDelegate <NSObject>
- (CGFloat)widthForColumnAtIndex:(NSInteger)columnIndex;
- (CGFloat)heightForRowAtIndex:(NSInteger)columnIndex;


NSInteger halfDivideFindIndexOfClosestValueInFloatArray(NSArray *array, CGFloat value) {
   NSInteger bottomIndex = 0;
   NSInteger topIndex = array.count - 1;
   while ((topIndex - bottomIndex) != 1) {
       NSInteger centerIndex = (topIndex + bottomIndex)/2;
       CGFloat centerValue = [array[centerIndex] floatValue];
       if (value > centerValue) {
           bottomIndex = centerIndex;
 else if (value < centerValue) {
           topIndex = centerIndex;
       else {
           bottomIndex = centerIndex;
   return bottomIndex;
@implementation PSFreezedColumnsLayout {
NSInteger _rowsCount;
   NSInteger _columnsCount;
   NSMutableArray *_columnWidths;
   NSMutableArray *_rowHeights;
   NSMutableArray *_columnXPositions;
   NSMutableArray *_rowYPositions;
   CGSize _contentSize;
   NSInteger _lastLoadedRow;
 BOOL _resetFlag;


-(id)initWithCoder:(NSCoder *)aDecoder {
   if (self = [super initWithCoder:aDecoder]) {
      _columnWidths = [NSMutableArray array];
       _columnXPositions = [NSMutableArray array];
       _rowHeights = [NSMutableArray array];
       _rowYPositions = [NSMutableArray array];
   return self;

- (void)setupColumns {
   _contentSize = CGSizeZero;
   _lastLoadedRow = -1;
   _columnsCount = self.collectionView.numberOfSections;
   _rowsCount = [self.collectionView numberOfItemsInSection:0];
   for (NSInteger section = 0; section < _columnsCount; ++section) {
       NSAssert([self.collectionView numberOfItemsInSection:0] == _rowsCount,
                @"All sections should have the same items number");
       CGFloat width = [self.delegate widthForColumnAtIndex:section];
       _columnWidths[section] = @(width);
       _columnXPositions[section] = @(_contentSize.width);
       _contentSize.width += width;

-(void)prepareLayout {
   if (!_columnsCount) {
       [self setupColumns];
   [self loadMoreRowSizesIfNeeded];
- (void)loadMoreRowSizesIfNeeded {
   CGPoint const contentOffset = self.collectionView.contentOffset;
   if (_contentSize.height - contentOffset.y < self.pagesLeftToLoadBlock * self.collectionView.bounds.size.height) {
       for (NSInteger row = _lastLoadedRow + 1; row < _rowsCount; ++row) {
           CGFloat height = [self.delegate heightForRowAtIndex:row];
           _rowHeights[row] = @(height);
           _rowYPositions[row] = @(_contentSize.height);
           _contentSize.height += height;
           _lastLoadedRow = row;
           if (_contentSize.height - contentOffset.y >= self.pagesBlockSize * self.collectionView.bounds.size.height) {

- (NSSet *)indexPathsForElementsInRect:(CGRect)rect 
   NSMutableSet *retValue = [NSMutableSet set];
   NSInteger startRow = halfDivideFindIndexOfClosestValueInFloatArray(_rowYPositions, rect.origin.y);
   NSInteger endRow = halfDivideFindIndexOfClosestValueInFloatArray(_rowYPositions, rect.origin.y + rect.size.height) + 1;
   NSInteger startCol = halfDivideFindIndexOfClosestValueInFloatArray(_columnXPositions, rect.origin.x);
   NSInteger endCol = halfDivideFindIndexOfClosestValueInFloatArray(_columnXPositions, rect.origin.x + rect.size.width) + 1;
   for (NSInteger row = startRow; row <= endRow; ++row) {
       for (NSInteger column = startCol; column <= endCol; ++column) {
           [retValue addObject:[NSIndexPath indexPathForRow:row inSection:column]];
       for (NSInteger column = 0; column < self.fixedColumnsCount; ++column) {
           [retValue addObject:[NSIndexPath indexPathForRow:row inSection:column]];
   return retValue;

- (NSArray *)layoutAttributesForElementsInRect:(CGRect) rect {
   [self loadMoreRowSizesIfNeeded];
   NSMutableArray *attributes = [NSMutableArray array];
   for (NSIndexPath *indexPath in [self indexPathsForElementsInRect:rect]) {
       UICollectionViewLayoutAttributes *layout = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
       CGPoint contentOffset = self.collectionView.contentOffset;
       CGPoint origin = CGPointMake([_columnXPositions[indexPath.section] floatValue],
                                    [_rowYPositions[indexPath.row] floatValue]);
       if (indexPath.section < self.fixedColumnsCount) {
           origin.x += contentOffset.x;
           static const NSInteger zLevelAbove = 1;
           layout.zIndex = zLevelAbove;
       layout.frame = (CGRect)
           .origin = origin,
           .size = CGSizeMake([_columnWidths[indexPath.section] floatValue],
                              [_rowHeights[indexPath.row] floatValue])
       [attributes addObject:layout];
   return attributes;
- (CGSize)collectionViewContentSize {
   return _contentSize;
- (void)dropLayoutCache {
   _columnsCount = 0;
   [self invalidateLayout];

- (void)dropLayoutCacheFromRowIndex:(NSInteger)index {
   NSRange rangeToDeleteFromArrays = NSMakeRange(index, _rowHeights.count - index + 1);
   [_rowHeights removeObjectsInRange:rangeToDeleteFromArrays];
   [_rowYPositions removeObjectsInRange:rangeToDeleteFromArrays];


The recipe of the Collection View customization

  1. Pick up a class to inherit. If you need to change the layout as a user scrolls, use UICollectionViewFlowLayout. For static cases a more lightweight UICollectionViewLayout can be used.
  2. Override the method collectionViewContentSize to return the collection content size (mind that it can change with time).
  3. If needed, override prepareLayout for preliminary calculations (for example, to calculate the same content size described above)
  4.  Override the method layoutAttributesForElementsInRect:

a) define which cells should get into a currently visible rectangle;

b) calculate coordinates and other attributes for those cells (don’t forget you can use any parameters here, for example contentOffset of the collection, tilt of the device, the position of the Moon in the sky) :)

    5. ….

    6. PROFIT!

Check out this video to see the result of my work:

Maybe UICollectionView isn't perfect for now, but there is always a way to experiment and make things possible. 

No reviews yet
Remember those Facebook reactions? Well, we aren't Facebook but we love reactions too. They can give us valuable insights on how to improve what we're doing. Whould you tell us how you feel about this article?