Reputation: 18729
I am working with a UICollectionView
using UICollectionViewFlowLayout
and have some difficulties to understand item sizing an spacing. I know that there several methods to adapt sizing and spacing (using the delegate methods, overriding FlowLayout, etc.). However without understanding the logic behind these values in the first place, it is quite hard to adapt them properly.
The following results have been created a default UICollectionViewController
with a default UICollectionViewCell
without any subclasses. Only the following settings haven been made:
Code:
private let reuseIdentifier = "Cell"
class MyViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
var layout: UICollectionViewFlowLayout {
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 5
layout.sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 0, right: 20)
layout.sectionInsetReference = .fromContentInset
return layout
}
collectionView.collectionViewLayout = layout
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
return cell
}
/*func collectionView(_ collectionView : UICollectionView,layout collectionViewLayout:UICollectionViewLayout,sizeForItemAt indexPath:IndexPath) -> CGSize {
var width = view.frame.width
return CGSize(width: collectionVw.frame.size.height, height: collectionVw.frame.size.height)
}*/
}
Using different values for layout.minimumInteritemSpacing = 5
creates results I do not understand:
...sizeForItemAt
to specify explicit dimensions. However, setting the size in IB should also work, shouldn't it? Why is the IB size of 200 x 200px ignored? Why 50x50, is this the default size or where is this specified?minimumInteritemSpacing
does not set the explicit spacing but only a minimum. However, how is the value computed?7.5px
for a value of 5? Why result values 10-25 result all in the same a spacing of 26.5px
?So: How exactly are sizes and spacing computed here?
Upvotes: 0
Views: 901
Reputation: 77477
As you've already learned from Larme, your Storyboard layout is ignored because you create a new layout.
When the collection view lays out its cells, it says:
.itemSize
?.zero
(your cell has no constraints affecting its size).estimatedItemSize
In your case, you set layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
which means "use the default of (50, 50)
".
"So: How exactly are sizes and spacing computed here?"
Well, let's take a look at some more cells.
We'll use 6 sections...
.minimumInteritemSpacing = 5.0
.minimumInteritemSpacing = 10.0
.minimumInteritemSpacing = 30.0
and we'll alternate between 3 and 7 items per section. Looks like this:
The answer is now pretty obvious - the collection view calculates how many total cells will fit (not how many cells are in the section) - based on cell-width plus minSpacing - and then increases the spacing for a "perfect fit."
So, assuming a collection view frame width of 320.0
and section insets of 20.0
on each side, we can calculate the adjusted item spacing:
min item spacing = 5.0
// CellWidth + MinSpacing
50.0 + 5.0 = 55.0
// collViewWidth - sectionInsets
320.0 - (20.0 + 20.0) = 280.0
// spacing is only BETWEEN cells, not at end, so
// add spacing
280.0 + 5.0 = 285.0
// how many cells will fit?
285.0 / 55.0 = 5.1818181818181817
// can't have a "partial-cell" so
floor(285.0 / 55.0) = 5.0
numCells = 5
// width of 5 cells
50.0 * numCells = 250.0
// "extra" space
280.0 - 250.0 = 30.0
// with 5 cells fitting, we have 4 "spaces" (numCells - 1)
30.0 / 4.0 = 7.5
actual spacing = 7.5
min item spacing = 10.0
// CellWidth + MinSpacing
50.0 + 10.0 = 60.0
// collViewWidth - sectionInsets
320.0 - (20.0 + 20.0) = 280.0
// spacing is only BETWEEN cells, not at end, so
// add spacing
280.0 + 10.0 = 290.0
// how many cells will fit?
290.0 / 60.0 = 4.833333333333333
// can't have a "partial-cell" so
floor(290.0 / 60.0) = 4.0
numCells = 4
// width of 4 cells
50.0 * 4.0 = 200.0
// "extra" space
280.0 - 200.0 = 80.0
// with 4 cells fitting, we have 3 "spaces"
80.0 / 3.0 = 26.666666 (rounded to 26.5 on @2x device scale)
actual spacing = 26.5
min item spacing = 30.0
// CellWidth + MinSpacing
50.0 + 30.0 = 80.0
// collViewWidth - sectionInsets
320.0 - (20.0 + 20.0) = 280.0
// spacing is only BETWEEN cells, not at end, so
// add spacing
280.0 + 30.0 = 310.0
// how many cells will fit?
310.0 / 80.0 = 3.875
// can't have a "partial-cell" so
floor(250.0 / 80.0) = 3.0
numCells = 3
// width of 3 cells
50.0 * 3.0 = 150.0
// "extra" space
280.0 - 150.0 = 130.0
// with 3 cells fitting, we have 2 "spaces"
130.0 / 2.0 = 65.0
actual spacing = 65.0
Here is some example code to see that result...
Simple cell with centered label:
class CenterLabelCell: UICollectionViewCell {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
label.backgroundColor = .yellow
label.font = .systemFont(ofSize: 16.0, weight: .light)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(label)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: g.centerXAnchor),
label.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
}
Example view controller:
class CollViewSpacingVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
var collectionView: UICollectionView!
let itemSpaces: [CGFloat] = [
5.0, 10.0, 30.0,
]
let sectionColors: [UIColor] = [
.systemRed, .systemRed.withAlphaComponent(0.5),
.systemGreen, .systemGreen.withAlphaComponent(0.5),
.systemBlue, .systemBlue.withAlphaComponent(0.5),
]
override func viewDidLoad() {
super.viewDidLoad()
var layout: UICollectionViewFlowLayout {
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
layout.minimumLineSpacing = 4
layout.scrollDirection = .vertical
layout.sectionInset = .init(top: 20.0, left: 20.0, bottom: 0.0, right: 20.0)
return layout
}
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
collectionView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
collectionView.widthAnchor.constraint(equalToConstant: 320.0),
])
collectionView.register(CenterLabelCell.self, forCellWithReuseIdentifier: "c")
collectionView.dataSource = self
collectionView.delegate = self
collectionView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return itemSpaces[section / 2]
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return sectionColors.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return section % 2 == 0 ? 3 : 7
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "c", for: indexPath) as! CenterLabelCell
cell.label.text = "\(Int(itemSpaces[indexPath.section / 2]))"
cell.contentView.backgroundColor = sectionColors[indexPath.section]
return cell
}
}
Upvotes: 1
Reputation: 26036
You have multiple questions (not really recommended), I'll answer what I can do.
Why are the items 50 x 50px in size? I know that one can use ...sizeForItemAt to specify explicit dimensions. However, setting the size in IB should also work, shouldn't it? Why is the IB size of 200 x 200px ignored? Why 50x50, is this the default size or where is this specified?
You are doing:
collectionView.collectionViewLayout = layout
Where layout is newly created.
You aren't using the previous settings in InterfaceBuilder, you are overriding them by code.
And your created for layout
, its itemSize
hasn't been set. And from the doc, if not set, it's 50x50.
Why are the items aligned to the left for these values and distribued evenly of the complete width for value of 30?
Are you sure about that? Default Layout (meaning, not a inherited from UICollectionViewFlowLayout
) will behave like paragraphs styling.
I'll take the horizontal layout (same logic can be applied in vertical, but analogy with text paragraphs would be strange):
If you have multilines text, the first line would take as much width as possible, but the last line will not, keeping it "left" aligned.
For your spacing calculations, according to the doc of minimumInterItemSpacing
:
For a vertically scrolling grid, this value represents the minimum spacing between items in the same row. For a horizontally scrolling grid, this value represents the minimum spacing between items in the same column. This spacing is used to compute how many items can fit in a single line, but after the number of items is determined, the actual spacing may possibly be adjusted upward.
But, I'm wondering, what would happen if you override viewDidLayoutSubview()
, and call collectionView.collectionViewLayout?.invalidateLayout(); collectionView.collectionViewLayout?.prepareLayout()
.
Upvotes: 1