I am writing a demo about the difference using compositional layout & traditional flow layout with UICollectionView
.
I found that if I want to set the cell’s width equal to the UICollectionView
’s width , but the cell’s height is determined by the cell’s content (self-sizing only in vertical).
This can be easily with compositional layout
let height = NSCollectionLayoutDimension.estimated(144)
let itemSize = NSCollectionLayoutSize(widthDimension: NSCollectionLayoutDimension.fractionalWidth(1), heightDimension: height)
But, if I use traditional flow layout, I can only set flow layout’s estimatedItemSize
like this:
layout.estimatedItemSize = CGSizeMake(collectionView.width, 140)
But, after layout, the cell’s final width will always determined by the actual content, not the width equal to collectionView
’s width.
Is there any best practice about this kind vertical-only self-sizing?
There are a variety of ways. One is to implement preferredLayoutAttributesFitting(_:)
, using required
for width, but a low priority for height:
class Cell: UICollectionViewCell {
@IBOutlet weak var label: UILabel!
override func preferredLayoutAttributesFitting(
_ layoutAttributes: UICollectionViewLayoutAttributes
) -> UICollectionViewLayoutAttributes {
let attributes = super.preferredLayoutAttributesFitting(layoutAttributes)
let size = CGSize(width: superview!.bounds.width - 40, height: 40)
attributes.frame.size = contentView.systemLayoutSizeFitting(
size,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .defaultLow
)
return attributes
}
}
That yields:
The full MRE:
class Cell: UICollectionViewCell {
@IBOutlet weak var label: UILabel!
override func preferredLayoutAttributesFitting(
_ layoutAttributes: UICollectionViewLayoutAttributes
) -> UICollectionViewLayoutAttributes {
let attributes = super.preferredLayoutAttributesFitting(layoutAttributes)
let size = CGSize(width: superview!.bounds.width - 40, height: 40)
attributes.frame.size = contentView.systemLayoutSizeFitting(
size,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .defaultLow
)
return attributes
}
}
class ViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
// some strings of increasing lengths
let strings = (0...15).map {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
var value = 0.0
for i in 0 ... $0 {
value += pow(10.0, Double(i))
}
return formatter.string(for: value)!
}
var dataSource: UICollectionViewDiffableDataSource<Section, String>!
override func viewDidLoad() {
super.viewDidLoad()
dataSource = .init(collectionView: collectionView) { [weak self] collectionView, indexPath, itemIdentifier in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
cell.label.text = self?.strings[indexPath.item]
return cell
}
collectionView.dataSource = dataSource
applySnapshot()
}
func applySnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
snapshot.appendSections([.main])
snapshot.appendItems(strings)
dataSource.apply(snapshot)
}
}
extension ViewController {
enum Section: Hashable {
case main
}
}