Reputation: 11
How to calculate the cell size so that different devices display the same number of cells in row and column, and the cell height is adjusted to the screen size of the device?
I tried calculating it this way, but it's not right.
Where did I make a mistake?
override func viewDidLoad() {
super.viewDidLoad()
let homeFlowLayout = UICollectionViewFlowLayout()
let itemsPerRow: CGFloat = 2
let itemsPerColumn: CGFloat = 2
let window = UIApplication.shared.connectedScenes.first as? UIWindowScene
let tabBarHeight = window?.windows.first?.safeAreaInsets.bottom ?? 0
let availableWidth = UIScreen.main.bounds.width
let width = availableWidth / itemsPerRow
let availableHeight = UIScreen.main.bounds.height - tabBarHeight
let height = availableHeight / itemsPerColumn
homeFlowLayout.itemSize = CGSize(width: width, height: height)
homeFlowLayout.minimumLineSpacing = 10
homeFlowLayout.scrollDirection = .vertical
collectionView.setCollectionViewLayout(homeFlowLayout, animated: false)
}
Upvotes: 0
Views: 170
Reputation: 438212
Ideally, rather than calculating the size ourselves, we would rather let the framework do it.
Now, this may be beyond the scope of what you might contemplate, but compositional layouts (as described in the Apple Implementing Modern Collection Views sample and in WWDC 2020’s Advances in Collection View Layout) gracefully handle heights as a percentage of the collection view (as well as orthogonal scrolling which you posted in a prior question).
To get five rows, I just defined the fractionalHeight
of the group to be ⅕ (and set the orthogonalScrolling
behavior):
func createLayout() -> UICollectionViewCompositionalLayout {
let itemSize = NSCollectionLayoutSize(
widthDimension: .estimated(100),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .estimated(100),
heightDimension: .fractionalHeight(1/5)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
return UICollectionViewCompositionalLayout(section: section)
}
That yields:
The only trick that I employed was regarding the spacing. If I set the height of each group to be ⅕ and added spacing, then the total height was too tall by the sum of the spacing. So, I just left it with no spacing, but then inset the content within my cells (the images in my example) to achieve the desired spacing. Maybe there is another way to achieve exactly ⅕ height including spacing, but it was not jumping out at me.
I freely acknowledge that if you have not played around with compositional layouts before, this might be a lot to take in. But when you throw in the orthogonal scrolling (if, indeed, that is still desired), then I think compositional layout really starts to shine. I’ve done the old “collection view inside a table view cell” technique, but it gets mightily ugly very quickly. The compositional layout might feel alien at first, but it is worth it, IMHO.
The aforementioned sample code has a rich array of examples, so I would encourage you to check that out.
All that having been said, if you wanted to calculate the cell sizes yourself using traditional techniques, there are a variety of approaches one could adopt.
But we would avoid using UIScreen.main
. It might feel convenient at this point, but will break if we later support iPads (with their split-screen multitasking), device rotations, etc. We should perform calculations on the basis of the bounds
of the content view, not the device’s main screen. That also largely eliminates brittle code that subtracts out the height of navigation bars, tab bars, etc. Just use the bounds
of the collection view. Perhaps:
func updateItemSize() {
let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
let rowCount: CGFloat = 5
let columnCount: CGFloat = 10
let spacing = layout.minimumInteritemSpacing
let insets = layout.sectionInset
layout.itemSize = CGSize(
width: (collectionView.bounds.width - (columnCount - 1) * spacing - insets.left - insets.right) / columnCount,
height: (collectionView.bounds.height - (rowCount - 1) * spacing - insets.top - insets.bottom) / rowCount
)
}
The next question is where you call this. The problem is that viewDidLoad
is too early in the view rendering process, so the bounds
of the collection view may not yet be reliable. There are lots of approaches, but you could do it in viewDidLayoutSubviews
:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateItemSize()
}
There are other approaches, but the key observation is that (a) you should just use the bounds
of the collection view; but that (b) viewDidLoad
is too early in the process.
Upvotes: 0