The main challenge here is that its hard to come up with pixel perfect border/cell measurements due to screen WIDTH limitations. ie, there is almost always leftover pixels that cannot be divided. For example, if you want a 3-column grid with 1px inner borders, and the screen width is 375px, it means each column width has to be 124.333 px, which is not possible, so if you round down and use 124px as width, you ll have 1 px leftover. (Isnt it crazy how much that extra minuscule pixel affects the design?)
An option is to pick a border width that gives you perfect numbers, however thats not ideal cause you should not let chance dictate your design. Also, it potentially wont be a future proof solution.
I looked into the app that does this best, instagram, and as expected, their borders look pixel perfect. I checked out the width of each column, and it turns out, the last column is not the same width, which confirmed my suspicion. For the human eye its impossible to tell that a grid column is narrower or wider by a pixel, but its DEFINITELY visible when the borders are not the same width. So this approach seems to be the best
At first I tried using a single reusable cell view and update the width depending whether is the last column or not. This approach kinda worked, but the cells looked glitchy, because its not ideal to resize a cell. So I just created an additional reusable cell that would go at the last column to avoid resizing.
First, set the width and spacing of your collection and register 2 reusable identifiers for your collection view cell
let borderWidth : CGFloat = 1
let columnsCount : Int = 3
let cellWidth = (UIScreen.main.bounds.width - borderWidth*CGFloat(columnsCount-1))/CGFloat(columnsCount)
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = CGSize(width: cellWidth, height: cellWidth)
layout.minimumLineSpacing = borderWidth
layout.minimumInteritemSpacing = borderWidth
collectionView = UICollectionView(frame: self.frame, collectionViewLayout: layout)
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: lastReuseIdentifier)
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
Then create a helper method to see if a cell at a certain index path is in the last column
func isLastColumn(indexPath: IndexPath) -> Bool {
return (indexPath.row % columnsCount) == columnsCount-1
}
Update the cellForItemAt
delegate method for your collection view, so the last column cell is used
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let identifier = isLastColumn(indexPath: indexPath) ? lastReuseIdentifier : reuseIdentifier
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
return cell
}
And finally, update the size of the cells programatically, so the last column cell takes over all the leftover available space
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = isLastColumn(indexPath: indexPath) ? UIScreen.main.bounds.width-(cellWidth+borderWidth)*CGFloat(columnsCount -1) : cellWidth
return CGSize(width: width, height: cellWidth)
}
The end result should look something like this (with bonus food pics):
