(I tested this with iOS 9, 10, and 11)
In newer versions of iOS (maybe 9?), you can do away with custom height calculation of collection view cells, and rely entirely on flow layout’s estimatedItemSize
function and autolayout to automatically find the correct height and width. However, sometimes a custom, manual, height calculation is required. Examples would be when you want to fit the width of a cell the entire width of the screen, or when you want to force a height to be 0, then you’ll need manual calculation.
As a reference for myself and others, I documented this down so no one will need to fiddle with this again in the future. So assuming you have a collection view in place, and a custom cell subclass, let’s put in the pieces to make the above image work.
Reference Code
Within the ViewController class:
// 1.
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout
- Whichever class you have that’s going to be doing the height calculation needs to conform to
UICollectionViewDelegateFlowLayout
// 2.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! TestCell
configure(cell: cell)
return cell
}
// 3.
func configure(cell: TestCell) {
cell.titleLabel.text = "Ooh woo, I'm a rebel just for kicks, now. I been feeling it since 1966, now. Might've had your fill, but you feel it still"
cell.subtitleLabel.text = "Feel It Still, by Portugal. The Man. Pretty good song I'm listening to while writing the configure method of this cell. "
}
// 4.
let sizingCell = TestCell()
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// 5.
let width = collectionView.frame.size.width
sizingCell.contentView.bounds.size.width = width
// 6.
configure(cell: sizingCell)
// 7.
sizingCell.contentView.setNeedsLayout()
sizingCell.contentView.layoutIfNeeded()
// 8.
let height = sizingCell.contentView.systemLayoutSizeFitting(CGSize(width: width, height: UILayoutFittingCompressedSize.height)).height
// 9.
sizingCell.prepareForReuse()
return CGSize(width: width, height: height)
}
2. In your cellForItemAt
method, dequeue a TestCell cell and configure it with a common configure method that the item height method can also later use.
3. Create a common cell configuration method, so that the cell set up and the cell height calculation methods use the same one.
4. Create an instance of the TestCell
, to be used only for cell size calculations. Do not use this cell in the collection view dequeue.
5. This is the key part of the cell height calculation. All cell subviews are added to its contentView
, so we’re essentially trying to answer the question – if the cell’s width is the entire screen width, what would its height be? So first make the cell’s bounds width the collection view width.
6. Call the common configure(_ cell:)
method that was used within the cell dequeue. Normally you would also pass in some view model to configure with too, but let’s keep it simple here.
7. We’ve got the cell’s bounds set to the right width, its content is filled in (2 multiline text labels in this case), so now we want to ask the cell to lay itself out again, so that the word wrap is correct on the text labels.
8. Use systemLayoutSizeFitting
to calculate the theoretical autolayout size and height of the cell. Note that it’s on the cell’s content view.
9. Call prepareForReuse()
so that the sizing cell can clean up anything it needs to. This normally gets called automatically if the cell is dequeued, but we’re using a single instance here to do our height calculations, so we need to call this ourselves.
Within the TestCell class
class TestCell: UICollectionViewCell {
let titleLabel: UILabel
let subtitleLabel: UILabel
override init(frame: CGRect) {
// 10.
titleLabel = UILabel()
titleLabel.textAlignment = .left
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.backgroundColor = UIColor.green
subtitleLabel = UILabel()
subtitleLabel.textAlignment = .left
subtitleLabel.numberOfLines = 0
subtitleLabel.lineBreakMode = .byWordWrapping
subtitleLabel.backgroundColor = UIColor.red
super.init(frame: frame)
contentView.backgroundColor = UIColor.white
contentView.addSubview(titleLabel)
contentView.addSubview(subtitleLabel)
configureLabels()
}
func configureLabels() {
// 11.
titleLabel.translatesAutoresizingMaskIntoConstraints = false
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
// 12.
let top = titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20)
top.priority = UILayoutPriority(999)
let leading = titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10)
let trailing = titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10)
NSLayoutConstraint.activate([top, leading, trailing])
NSLayoutConstraint.activate([
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
subtitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
subtitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
subtitleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// 13.
override func layoutSubviews() {
super.layoutSubviews()
titleLabel.preferredMaxLayoutWidth = titleLabel.frame.size.width
subtitleLabel.preferredMaxLayoutWidth = subtitleLabel.frame.size.width
super.layoutSubviews()
}
}
10. Basic cell setup. The big thing to note here is setting numberOfLines = 0
and lineBreakMode = .byWordWrapping
.
11. Setting up the label constraints. Some very important points here. First, if you use the layout anchors to setup constraints you must set translatesAutoresizingMaskIntoConstraints = false
on all the subviews that you’re adding constraints to, but not on its superview. So here, we don’t set it on the content view, only the labels.
12. Setting up the actual constraints using layout anchors. That’s a whole other subject in itself. You can use whatever approach you like. I’ve been using layout anchors and like them. The only drawback is setting the priority is a pain. Here we set one of the vertical distance constraints to priority = 999 to avoid the console complaining about constraints breaking. When the cell first appears, its height is briefly 0, causing default constraints with priority 1000 to break. When you set it to 999, it still enforces a high priority, without the console shitting itself.
13. Another really important piece. Labels don’t know how wide you want to draw its text. You’d assume this would be default, but no. So the first super.layoutSubviews()
call is to draw the labels according to autolayout so they’re the correct width. Then we set the preferredMaxLayoutWidth
to the frame width so that the inner text is wraps with the width of the label. Then we call super.layoutSubviews()
to get the overall autolayout correct with word wrapping involved.
Leave a Reply
Want to join the discussion?Feel free to contribute!