7

I am using a UICollectionView with a custom layout that lays out cells in a grid format. There can be well over 50 rows and 50 columns. Scrolling occurs both vertically and horizontally. Currently, I am doing all of the layout setup in prepareLayout and storing it in arrays:

- (void)prepareLayout {

     NSMutableArray *newLayoutInfo = [[NSMutableArray alloc] init];
     NSMutableArray *newLinearLayoutInfor = [[NSMutableArray alloc] init];

     NSInteger sectionCount = [self.collectionView numberOfSections];
     NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];

     self.heightForRows = [delegate collectionViewHeightForAllRows];

     self.totalWidthsForRows = [[NSMutableArray alloc] init];

     for (int i = 0; i < sectionCount; i++) {
       [self.totalWidthsForRows addObject:[NSNumber numberWithInt:0]];
     }
     for (NSInteger section = 0; section < sectionCount; section++) {
       NSMutableArray *cellLayoutInfo = [[NSMutableArray alloc] init];


       NSInteger itemCount = [self.collectionView numberOfItemsInSection:section];

     for (NSInteger item = 0; item < itemCount; item++) {
        indexPath = [NSIndexPath indexPathForItem:item inSection:section];

        UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
        itemAttributes.frame = [self frameForCellAtIndexPath:indexPath];

        [cellLayoutInfo addObject:itemAttributes];
        [newLinearLayoutInfor addObject:itemAttributes];
    }
    [newLayoutInfo addObject:cellLayoutInfo];
}
self.layoutInfo = newLayoutInfo;
self.linearLayoutInfo = newLinearLayoutInfor;
}

Then in layoutAttributesForElementsInRect I have:

- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *rows = [self.linearLayoutInfo filteredArrayUsingPredicate:[NSPredicate    predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes *evaluatedObject, NSDictionary *bindings) {
    return CGRectIntersectsRect(rect, [evaluatedObject frame]);
}]];

This works okay, but it is laggy and jumpy when I have over 50 columns and 50 rows. The problem I now have is that I must set

-(BOOL)shouldInvalidateLayoutForBoundsChange {
       return YES;
} 

This makes it prepare the entire layout every time the bounds change, which, needless to say, has a huge impact on performance and you can barely scroll. The cells consist of just text with an opaque background, so there is no issue there.

I am sure I am not doing this right and that there must be a better way. Thanks for the help in advance.

Mason Wolters
  • 514
  • 2
  • 6
  • 13
  • 2
    You should use Instruments to see where you are spending your time during scrolling. – nielsbot Mar 02 '13 at 18:46
  • I recommend laying out elements as they are added to your collection view. No need to layout elements that already are there--they're in the correct positions. If you do it this way you can turn off `shouldInvalidateLayoutForBoundsChange` – nielsbot Mar 02 '13 at 18:48
  • @nielsbot I have supplementary views that are acting as headers that must always be set to the content offset of the collectionview. These are on the top and side of the view. If I turn of `shouldInvalidateLayoutForBoundsChange` then the headers no longer "float" where they are supposed to. Is there a different solution to that? – Mason Wolters Mar 02 '13 at 19:18
  • Are they fixed on the screen? Why not just put them inside the collection view's parent view? If they work as headers, use collection view's header support. If you need something more dynamic, you can set yourself as a scroll view delegate of your collection view and observe changes in it's scroll position. – nielsbot Mar 02 '13 at 20:28
  • I asked a different question about headers in collection views and I got [this answer](http://stackoverflow.com/questions/15159559/uicollectionview-floating-headers-on-top-and-side). Are you saying this is the incorrect way of doing it? I would prefer for the headers to be built into the collectionview instead of having views in the parent and changing their frames based on the scrolling of the collectionview. – Mason Wolters Mar 02 '13 at 20:40
  • ok--I understand now. Here's what I recommend: create 3 collection views... one for the column headers (where each cell is column header), one for the row leaders (each cell = 1 row leader) and one collection view for your cells. Then when the scroll position of any collection view is changed by the user, update the scroll positions for the other 2 collection views as appropriate. – nielsbot Mar 02 '13 at 21:32
  • I, too have this problem despite caching and optimizing layoutAttributesForElementsInRect. Seems that floating elements should be possible using a collection view layout, but in practice the performance is terrible. – akaru Mar 18 '13 at 23:45

3 Answers3

9

In custom flow layout I do this and it seems to help:

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return !(CGSizeEqualToSize(newBounds.size, self.collectionView.frame.size));
}
zumzum
  • 17,984
  • 26
  • 111
  • 172
  • 1
    Does this work? At this point it appears the collection view already has the new size, thus always equalling the newBounds's size. – mattyohe Sep 08 '15 at 18:37
5
-(BOOL)shouldInvalidateLayoutForBoundsChange {
       return YES;
} 

Causes the layout to do prepareLayout() every time it scrolls, which means anything of heavy computing in prepare will lead to a laggy practice, so one possible direction to solve this is to check what's really taking much time. One possibility is what's inside

for (NSInteger section = 0; section < sectionCount; section++)
{
   // generate attributes ...
}

in order to generate attributes for the layout. Every time it scrolls, every time this generalization reruns, so that it impacts on the scroll appear to be jumpy and clumsy. So in order to solve this issue, or at least sort out that this is not the really trouble, I suggest setting a flag in this layout algorithm, say, isScrolling, standing for the situation where the layout needs to prepare. Every time in prepareLayout() check the flag, if it is YES, then we'll know there's no need to do for loop to regenerate all the attributes, which alreay exsit ever since the first time the layout is initialised.

BabyPanda
  • 1,562
  • 12
  • 18
  • 1
    That's why you should implement logic in shouldInvalidateLayoutForBoundsChange to see if layout has to be invalidated. return YES is evil. – pronebird Nov 23 '14 at 14:40
0

ok--I understand now. Here's what I recommend: create 3 collection views... one for the column headers (where each cell is column header), one for the row leaders (each cell = 1 row leader) and one collection view for your cells. Then when the scroll position of any collection view is changed by the user, update the scroll positions for the other 2 collection views as appropriate.

enter image description here

nielsbot
  • 15,922
  • 4
  • 48
  • 73
  • you should use Instruments to profile your app during scrolling and see where you are spending your CPU time. Doing anything CPU intensive on the main thread can kill your scrolling performance. – nielsbot Mar 05 '13 at 23:06
  • 2
    I also made a profiler you can use during your draw/layout routines that might help: https://github.com/nielsbot/Profiler – nielsbot Mar 05 '13 at 23:06
  • 4
    Are you kidding? 3 collection views?? Collection view is meant to design any kind of UI with its custom layout subclassing. Creating 3 collection views simply means killing the performance of the app. – Anil Kumar May 16 '14 at 15:57
  • No, I am not kidding. I would try it to see the implications before I just assume it's too slow. And obviously collection view isn't working for this particular case. – nielsbot May 16 '14 at 18:04
  • I know this is an old thread, but I've been working on some very similar problems and thought I could add some extra data here. Multiple views with scroll delegates linked provides a slight delay in the scrolling. Whichever views are "linked" will appear just a bit behind the others. The fully custom layout appears to be the way to go for a very native responsive feel. – Kyle Jan 28 '15 at 19:26
  • @Kyle can't you set the scroll offset of the matching scroll views in the main scroll view's *setOffset:*? No need to invalidate layouts or use delegates – nielsbot Jan 29 '15 at 21:53
  • @nielsbot you'll get the same effect. The scroll you are "touching" will be slightly ahead of the other scrolls. – Kyle Jan 29 '15 at 22:22