-->

sizeForItemAtIndexPath getting called for all inde

2019-08-14 16:35发布

问题:

I am able to reproduce this on a very simple standalone app.

I have a collectionView which I want to make circular/loop so the elements repeat again and again (aka when user is at the last element in array, it shows the first element again after. And if they are at the first and scroll left, it shows the last element again). So it's a never ending collectionView.

So for a simple example, let's use days of week:

....Sunday, Monday, Tuesday, Wednesday..Saturday, Sunday, Monday....

In order to achieve this, I return a big number (10000) in the numberOfItemsInSection and use indexPath.item%7 in the cellForItemAtIndexPath method to adjust and get the correct element. Using %7 as there are 7 days. My cells are very simple - just a UILabel in it.

This all works perfectly.

The issue comes with the sizeForItemAtIndexPath. I want the cells to fit the label. As there would only be 7 actual size variations, so I pre-cache the sizes of the 7 days in a dictionary and return the correct size in sizeForItemAtIndexPath method.

The problem is that (either due to a bug or intentional bad design by Apple of collectionview), the sizeForItemAtIndexPath gets calls for every indexPath before the collectionview appears. So if I want to have the circular collectionView logic and need to return the big number (10000), it's is calling sizeForItemAtIndexPath for all 10000 indexes. So there is a couple seconds lag until the collectionView appears. If I comment out the sizeForItemAtIndexPath, then it works instantly. So that's definitely the issue. I put a NSLog in the sizeForItemAtIndexPath and it logs all the 22222 calls before load.

I have even defined the setEstimatedItemSize, it still calls sizeForItemAtIndexPath for all indexes.

I can reduce the lag by returning a smaller number 1000 but still, this is a bad design or bug for sure.

TableView doesn't have this bug - you can define a million rows and it only calls the heightForRow when it actually needs it. So I am not sure why collectionView needs to call it for all cells before showing, especially if setEstimatedItemSize is already defined too.

Another side-effect of this bug is that the collectionView throws an error if I return a bigger value (50000 makes it break, 22222 is okay). It prints the error for too big values:

This NSLayoutConstraint is being configured with a constant that exceeds internal limits.  A smaller value will be substituted, but this problem should be fixed. Break on BOOL _NSLayoutConstraintNumberExceedsLimit(void) to debug.  This will be logged only once.  This may break in the future.

TableView can easily handle huge values because it doesn't have this bug.

I have also tried disabling prefetching but that had no effect.

What do you all think?

Relevant code:

#define kInfiniteCount 22222
#define kDayNameMargin 30

@interface ViewController (){
    NSMutableDictionary *dictOfSizes;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
    self.myCalendar = [NSCalendar currentCalendar];
    [self.myCalendar setLocale:locale];

    dictOfSizes = [NSMutableDictionary new];

    for (int i=0; i<7; i++) {
        WeekdayCollectionViewCell *sizingCell = [[NSBundle mainBundle] loadNibNamed:@"WeekdayCell" owner:self options:nil][0];
        sizingCell.myLabel.text=[self.myCalendar weekdaySymbols][i];
        [sizingCell layoutIfNeeded];

        [dictOfSizes setObject:[NSValue valueWithCGSize:[sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]] forKey:sizingCell.myLabel.text];
    }



    self.myCollectionView.decelerationRate = UIScrollViewDecelerationRateFast;
    [self.myCollectionView registerNib:[UINib nibWithNibName:@"WeekdayCell" bundle:nil] forCellWithReuseIdentifier:@"daycell"];
    [(UICollectionViewFlowLayout*)self.myCollectionView.collectionViewLayout setEstimatedItemSize:CGSizeMake(200, self.myCollectionView.frame.size.height)];
    [self.myCollectionView reloadData];


    NSInteger middleGoTo = kInfiniteCount/2;

    while (![[self.myCalendar weekdaySymbols][middleGoTo%7] isEqualToString:@"Monday"]) {
        middleGoTo--;
    }

    [self.myCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:middleGoTo inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return kInfiniteCount;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    WeekdayCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"daycell" forIndexPath:indexPath];

    cell.myLabel.text=[self.myCalendar weekdaySymbols][indexPath.item%7];
    return cell;
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{
    NSLog(@"sizeForItemAtIndexPath: %ld",indexPath.item);
    return [(NSValue*)[dictOfSizes objectForKey:[self.myCalendar weekdaySymbols][indexPath.item%7]] CGSizeValue];
}

EDIT:

Few people mentioned using scrollview instead of collectionview for this.

Pretty much everywhere I researched for the circular scrollview, they recommended using a collectionview for this purpose as it's much easier. My real requirement is a bit more complex which requires me to use collectionview too.

Scrollview requires you to use the scrollViewDidScroll method and change the content offset each time. Plus it loads all views in memory at once as it doesn't have the advantage of reusing existing cells like collectionview does. So that's another memory hit.

The 7 weekdays is a simple example I used. If someone wants to show a lot of data (100), that would be a pretty bad implementation in scrollview and collectionview will present this bug.

回答1:

That's expected behaviour and it's not so much related to UICollectionView - it is more UICollectionViewFlowLayout that you use.

With each cell being different size, FlowLayout will request sizes for each cell separately, to calculate total size of CollectionView - that is required to properly handle scrolling, scrollbars.

UITableView it's a bit simpler, as it's layout is much simpler (only height matters) - that's why it's possible to use estimatedSize there.

The whole Core Layout Process is well explained here:

https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CreatingCustomLayouts/CreatingCustomLayouts.html

To overcome this problem I would recommend using your custom UICollectionViewLayout and move your caching logic and reusing sizes for cells inside CollectionViewLayout, not in your ViewController.