0

I have an iOS 7 app using Storyboards that has the following structure:

UITabBarController->UINavgiationController->UICollectionViewController

I use a custom subclass of UICollectionViewFlowLayout to layout the grid. Each cell contains a single UILabel.

I am trying to render a 5 column grid in the UICVC. There can be upwards of 800 rows (sections, as one row per section). I've based this loosely on Erica Sadun's example code from her iOS book.

The cells all have specific, set widths (2 different widths used). All cells have the same height. The grid is wider than the physical display, and so scrolls horizontally and vertically.

It's all working fine, but performance has been very poor for more than about 30 rows. The problem comes when trying to calculate the custom layout, specifically in the following method:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSArray *attribs = [super layoutAttributesForElementsInRect:rect];

    NSMutableArray *attributes = [NSMutableArray array];

    [attribs enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *currentLayout, NSUInteger idx, BOOL *stop)
     {
         NSString *layoutItemKey = [NSString stringWithFormat:@"%ld:%ld", (long)currentLayout.indexPath.section, (long)currentLayout.indexPath.item];

         UICollectionViewLayoutAttributes *newLayout = [self.cachedLayoutAttributes objectForKey:layoutItemKey];

         if (nil == newLayout)
         {
             newLayout = [self layoutAttributesForItemAtIndexPath:currentLayout.indexPath];

             long section = currentLayout.indexPath.section;
             long item = currentLayout.indexPath.item;

             NSString *layoutItemKey = [NSString stringWithFormat:@"%ld:%ld", (long)section, (long)item];

             [self.cachedLayoutAttributes setObject:newLayout forKey:layoutItemKey];
         }

         [attributes addObject:newLayout];
     }];

    return attributes;
}

The main delay here seems to come from a call to the iOS method:

layoutAttributesForCellWithIndexPath:(NSIndexPath *)indexPath;

which is inside the layoutAttributesForItemAtIndexPath method:

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    CGSize thisItemSize = [self sizeForItemAtIndexPath:indexPath];

    CGFloat verticalOffset = [self verticalInsetForItemAtIndexPath:indexPath];
    CGFloat horizontalOffset = [self horizontalInsetForItemAtIndexPath:indexPath];

    if (self.scrollDirection == UICollectionViewScrollDirectionVertical)
        attributes.frame = CGRectMake(horizontalOffset, verticalOffset, thisItemSize.width, thisItemSize.height);
    else
        attributes.frame = CGRectMake(verticalOffset, horizontalOffset, thisItemSize.width, thisItemSize.height);

    return attributes;
}

Scrolling works fine to a point, then freezes for about 1.5 seconds while the next block of layout is calculated (always seems to be about 165 cells). As I'm caching everything, the next time the user scrolls performance is fine.

If I leave the cell widths to the UICollectionViewFlowLayout default everything flies, with no pauses.

To try to speed things up I have:

  • Ensured all views are opaque
  • Set the deceleration rate of the CollectionView to FAST
  • Made - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds return NO
  • I cache any UICollectionViewLayoutAttributes calculated
  • I cache the first 50 rows on initialisation. There's no noticeable lag doing this, and it allows the initial scroll performance to be a bit better than it would have been

I've run out of ideas for squeezing more performance out of the UICollectionViewFlowLayout.

Can anyone suggest how I can improve the code?

Thanks

Darren.

Darren Wheatley
  • 456
  • 1
  • 3
  • 14
  • When you calculate the attributes in layoutAttributesForElementsInRect, are you just calculating them for the passed in rect or are you calculating them for all cells? How many cells on the screen at once? – Matt Sep 05 '14 at 14:46
  • Thanks for the reply. I've edited the original question to add in the code for that method. I'm calling super at the start to get the layout attributes for the elements in the rect, which happens very quickly (using the default UICollectionViewFlowLayout is very fast). I then step through them and recalculate the new layout. The delay only comes when trying to calculate the new layout attributes as far as I can tell. – Darren Wheatley Sep 07 '14 at 10:48
  • so on the newLayout = [self layoutAttributesForItemAtIndexPath:currentLayout.indexPath]; line? Have you overridden that method too? – Matt Sep 08 '14 at 13:38
  • I have, again though mainly using Erica Sadun's example code. I've updated the question to add in the code. FYI, the layoutAttributesForCellWithIndexPath:(NSIndexPath *)indexPath; has not been overridden. – Darren Wheatley Sep 10 '14 at 16:08

1 Answers1

0

I've found a solution to the problem, and while it's not a complete answer to UICollectionView performance problems, it meets my needs.

I'm rendering a text grid. The rows are all the same height, the widths of the fields are always the same, and the overall width of a row is always the same.

Erica Sadun's code allows for variable height text cells, for text wrapping I assume. Because of that the layout needs to be calculated for each cell and row. For a grid with more than 100 cells the amount of time required is too long (on my iPhone 4S).

What I've done is to remove all the height and width calculation code, and replace it with fixed values I calculated manually.

This removes the bottleneck, and now I can render a grid with a few thousand cells without any noticeable lag.

Darren Wheatley
  • 456
  • 1
  • 3
  • 14