I'm trying to implement Facebook style image layout in Swift. I found
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)
}
}
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.
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.