Collection View Cells are resizing unintentionally when scrolling to bottom of CollectionView

2.5k Views Asked by At

I am building a collection view (gallery) of images.

The layout is 3 cells per row.

 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        let width: CGFloat = (view.frame.width / 3) - 8
        let height: CGFloat = width

        return CGSize(width: width, height: height)
    }

Everything looks great until I scroll to the bottom of the collection view.

it goes from: 3 per row, looks good

to: super blown up picture, larger than the screen

I also get this error message:

The behavior of the UICollectionViewFlowLayout is not defined because:

the item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values. 

Please check the values returned by the delegate.

The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7fa97f708c70>, and it is attached to <UICollectionView: 0x7fa981022000; frame = (0 0; 414 896); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x6000019986f0>; layer = <CALayer: 0x6000017a8820>; contentOffset: {0, 498.33333333333331}; contentSize: {414, 1250}; adjustedContentInset: {88, 0, 34, 0}; layout: <UICollectionViewFlowLayout: 0x7fa97f708c70>; dataSource: <MarsRover.GalleryCollectionVC: 0x7fa97f50b610>>.

any insight would be great, I want to turn this into infinite scrolling (api prefetching) too down the road, just fyi if that means I can ignore this.

3

There are 3 best solutions below

0
On

First of all, why not use bounds instead of frame?

Second, the reason this is happening is most likely because you are adjusting the layout elsewhere while the collectionView is loading cells (scrolling will make it load cells).

Are you using UIScrollViewDelegate methods to do anything with layout?

0
On

Since all of your items are they same size you should not be setting them in the delegate method. Instead set a static size on your flowLayout (you can drag an outlet from the storyboard) in viewDidLayoutSubviews and use the layout insets to do it so you can't possibly get the math wrong:

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        let remainingSpace = collectionView.bounds.width
                            - flowLayout.sectionInset.left
                            - flowLayout.sectionInset.right
                            - flowLayout.minimumInteritemSpacing * (Constant.numberOfItemsAcross - 1)
        let dimension = remainingSpace / Constant.numberOfItemsAcross
        flowLayout.itemSize = CGSize(width: dimension, height: dimension)
    }
0
On

The error you're getting is pointing at the item's width as the problem, it is saying, basically that the item(s) width and spacing cannot be more than the collection view's width. This is because your collection view has a vertical scrolling.

So to achieve a correct behavior for your collection view you must set your controller as the delegate for the collection view and also adopt the UICollectionViewDelegateFlowLayout. In your case I can see you've already implemented the collectionView(_:, layout:, sizeForItemAt: method.

In your implementation there is a clear intention to divide the collectionView's width in three equal parts. The calculation is considering a third part of the self.view.width minus eight. If I assume correctly you're intending to left an inter-item spacing of 8. If that's the case you should specify it into another method:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
    return 8
}

That will specify the inter-item spacing to 8 points.

Continuing with the cell's width and height, you must then divide the collectionView.frame.width but before that you must subtract the inter-spacing value from this quantity, because that is the remaining space for your cells.

So your implementation would be

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    // We subtract 16 because there are 2 inter item spaces like:
    // [CELL] [spacing:8] [CELL] [spacing:8] [CELL]
    let usableWidth = collectionView.frame.width - (2 * 8) 

    let cellWidth: CGFloat = usableWidth / 3
    let cellHeight: CGFloat = cellWidth

    return CGSize(width: cellWidth, height: cellHeight)
}

This should do the layout for you.