I'm currently working on an app where we need to show items in a grid, with a fixed number of columns. So a UICollectionView
with a custom UICollectionViewFlowLayout
is great for this!
I created the following custom UICollectionViewFlowLayout
that calculates the item size depending on the collectionView/container width, the desired number of columns and the desired spacing between the items (left of the first item, between the items, and right of the last item). So if we want two columns, it is going to be like this:
spacing - item - spacing - item - spacing
The custom UICollectionViewFlowLayout
import UIKit
class MultiColumnCollectionViewFlowLayout: UICollectionViewFlowLayout {
let columnCount: Int = 2
let itemAspectRatio: CGFloat = 0.71
let spacing: CGFloat = 8
var containerWidth: CGFloat = .zero {
didSet {
if containerWidth != oldValue {
self.invalidateLayout()
}
}
}
// MARK: - Init
override init() {
super.init()
self.configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.configure()
}
// MARK: - Layout
override func invalidateLayout() {
super.invalidateLayout()
self.configure()
}
func configure() {
self.scrollDirection = .vertical
self.itemSize = UICollectionViewFlowLayout.automaticSize
self.sectionInset = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
self.minimumLineSpacing = spacing
let numberOfSpacings = CGFloat(columnCount + 1)
let itemWidth: CGFloat = (containerWidth-(spacing*numberOfSpacings))/CGFloat(columnCount)
self.estimatedItemSize = CGSize(width: itemWidth, height: itemWidth*itemAspectRatio)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let att = super.layoutAttributesForElements(in: rect) else { return [UICollectionViewLayoutAttributes]() }
var x: CGFloat = sectionInset.left
var y: CGFloat = -1.0
for a in att {
if a.representedElementCategory != .cell { continue }
if a.frame.origin.y >= y { x = sectionInset.left }
a.frame.origin.x = x
x += a.frame.width + minimumInteritemSpacing
y = a.frame.maxY
}
return att
}
}
Problem:
With 1 item it works as expected:
With multiple items (let's say 3 items), it shows this (not expected):
What I want to happen, is something like this:
Fun fact: when I try this at an iPad with 3 columns, different spacing and different item aspect ratio, it works fine:
I debugged it and for the scenario with 2 columns on an iPhone 8 (iOS 13.3) (snapshots are taken on this device), itemAspectRatio 0.71 and spacing 8 (so the width of the collection view is 375 on an iPhone 8), the itemWidth should be 175.5. It is exactly that item width when I debug it, if you calculate 175,5+175,5+8+8+8 = 375, (2 items + 3 times spacing), so that should fit perfectly. For some reason, it doesn't. Let's say it is driving me nuts. Please help.