Home > Software design >  Swift - Facebook style image grid using UICollectionViewFlowLayout
Swift - Facebook style image grid using UICollectionViewFlowLayout

Time:12-30

I'm trying to implement Facebook style image layout in Swift. I found enter image description here

As you can see in the image above, however, there's an overflow: the three smaller cells seems to be taking up more than the screen width. However, if I subtract 0.1 to each of the cell's width, the problem seems to have been solved.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let item = indexPath.item
        let width = collectionView.bounds.size.width
        let padding:CGFloat = 5
        if item < 2 {
            let itemWidth:CGFloat = (width - padding) / 2.0
            return CGSize(width: itemWidth, height: itemWidth)
        } else {
            let itemWidth:CGFloat = (width - 2 * padding) / 3.0
            // let itemWidth:CGFloat = (width - 2 * padding) / 3.0 - 0.1  // This WORKS!!
            return CGSize(width: itemWidth, height: itemWidth)
        }
    }

enter image description here

I guess the overflow is caused by the value of itemWidth rounding up. But I feel like subtracting a random hard-coded value is not the best practice when dealing with this kind of issue.

Can anyone suggest a better approach for this?

Below is the full code that is reproducible.


class ContentSizeCollectionView: UICollectionView {
    override var contentSize: CGSize {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }
    
    override var intrinsicContentSize: CGSize {
        layoutIfNeeded()
        return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
    }
}

class GridVC: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDelegate, UICollectionViewDataSource {
    lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.minimumLineSpacing = 5
        layout.minimumInteritemSpacing = 5
        let collectionView = ContentSizeCollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "ident")
        return collectionView
    }()
    
    override func viewDidLoad() {
        view.addSubview(collectionView)
        NSLayoutConstraint.activate([
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let item = indexPath.item
        let width = collectionView.bounds.size.width
        let padding:CGFloat = 5
        if item < 2 {
            let itemWidth:CGFloat = (width - padding) / 2.0
            return CGSize(width: itemWidth, height: itemWidth)
        } else {
            let itemWidth:CGFloat = (width - 2 * padding) / 3.0
            return CGSize(width: itemWidth, height: itemWidth)
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 5
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ident", for: indexPath)
        cell.backgroundColor = .red
        return cell
    }
}

Update

floor((width - 2 * padding) / 3.0) did prevent the overflow, but it leaves a small gap at the end.

enter image description here

CodePudding user response:

Change this line:

let itemWidth:CGFloat = (width - 2 * padding) / 3.0

to this:

let itemWidth:CGFloat = floor((width - 2 * padding) / 3.0)

You may want to do that with a 2 item row as well.


Edit

The initial problem is that floating-point numbers are approximations.

So, on an iPhone 8, for example, the view width is 375 points. The cell width calculation of:

let padding: CGFloat = 5
let itemWidth:CGFloat = (width - 2 * padding) / 3.0

itemWidth ends up being 121.666... (repeating), which UIKit interprets as 121.66666666666667.

So, 121.66666666666667 * 3.0 10 equals (roughly) 375.00000000000011 and... that is greater than 375.0.

So, the collection view says "can't fit that on one row."

Using floor() fixes the problem, except... it hits a really weird bug!

If you change numberOfItemsInSection from 5 to 8, you'll get two rows of 3, and there will be no gap on the right.

We can get around this by making the side cells slightly narrower than the center cell like this:

// we want all three to be the same heights
//  1/3 of (width - 2 * padding)
let itemHeight: CGFloat = (width - 2 * padding) / 3.0

// left and right cells will be 1/3 of (width - 2 * padding)
//  rounded down to a whole number
let sideW: CGFloat = floor(itemHeight)

// center cell needs to be
//  full-width minus 2 * padding
//  minus
//  side-width * 2
let centerW: CGFloat = (width - 2 * padding) - sideW * 2
        
// is it left (0), center (1) or right (2)
let n = (item - 2) % 3
        
// use the proper width value
let itemWidth: CGFloat = n == 1 ? centerW : sideW

return CGSize(width: itemWidth, height: itemHeight)

Or, what seems to be working is making the width just slightly smaller than the floating-point 1/3rd. You used -0.1, but it also works with:

let itemWidth:CGFloat = ((width - 2 * padding) / 3.0) - 0.0000001

In any case, that hopefully explains the reason for the "2 cells instead of 3" issue, and two options for avoiding it.

  • Related