I\'ve read a bunch of posts on adding header to UICollectionView. In an iOS 7+ app in Swift, I\'m trying to add a header with a UILabel in it whose height should adjust base
The idea is to have a template header instance in memory to calculate the desired height before creating the result header view. You should move your section header view to a separate .nib file, setup all autolayout constraints and instantiate the template in your viewDidLoad method like this:
class MyViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
@IBOutlet var collectionView : UICollectionView?
private var _templateHeader : MyHeaderView
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "HeaderView", bundle:nil)
self.collectionView?.registerNib(nib, forCellWithReuseIdentifier: "header_view_id")
_templateHeader = nib.instantiateWithOwner(nil, options:nil)[0] as! MyHeaderView
}
}
Then you will be able to calculate the header size (height in my example) in your flow layout delegate method:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
_templateHeader.lblTitle.text = "some title here"
_templateHeader.lblDescription.text = "some long description"
_templateHeader.setNeedsUpdateConstraints();
_templateHeader.updateConstraintsIfNeeded()
_templateHeader.setNeedsLayout();
_templateHeader.layoutIfNeeded();
let computedSize = _templateHeader.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
return CGSizeMake(collectionView.bounds.size.width, computedSize.height);
}
And then create and return your regular header view as always, since you have already calculated its size in flow layout delegate method:
func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "header_view_id", forIndexPath: indexPath) as! MyHeaderView
headerView.lblTitle.text = "some title here"
headerView.lblDescription.text = "some long description"
headerView.setNeedsUpdateConstraints()
headerView.updateConstraintsIfNeeded()
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
return headerView
default:
assert(false, "Unexpected kind")
}
}
Forgot to say about one important moment - your header view should have an autolayout constraint on its contentView to fit the collection view width (plus or minus the desired margins).
In your cell add the following:
fileprivate static let font = UIFont(name: FontName, size: 16)
fileprivate static let insets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
Adjust both fonts and insets accordignly.
Then add the following to your cell
static func textHeight(_ text: String, width: CGFloat) -> CGFloat {
let constrainedSize = CGSize(width: width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)
let attributes = [ NSAttributedStringKey.font: font ]
let options: NSStringDrawingOptions = [.usesFontLeading, .usesLineFragmentOrigin]
let bounds = (text as NSString).boundingRect(with: constrainedSize, options: options, attributes: attributes as [NSAttributedStringKey : Any], context: nil)
return ceil(bounds.height) + insets.top + insets.bottom
}
You can now use this function to calculate automatic height
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
let height = ManuallySelfSizingCell.textHeight("Your text", width: view.frame.width)
return CGSize(width: view.frame.width, height: height + 16)
}
Instead of recreating a new label from code as shown in multiple answer, we can simply use the existing one to calculate the fitting size.
Code for your UICollectionViewDelegate
:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
// We get the actual header view
let header = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: IndexPath(row: 0, section: section)) as! MyHeaderView
// We ask the label what size it takes (eventually accounting for horizontal margins)
var size = header.myLabel.sizeThatFits(CGSize(width: collectionView.frame.width - horizontalMargins, height: .greatestFiniteMagnitude))
// We eventually account for vertical margins
size.height += verticalMargins
return size
}
Works for iOS 11+.
Like the questioner, I had a UICollectionView that contained a header with a single label, whose height I wanted to vary. I created an extension to UILabel
to measure the height of a multiline label with a known width:
public extension UILabel {
public class func size(withText text: String, forWidth width: CGFloat) -> CGSize {
let measurementLabel = UILabel()
measurementLabel.text = text
measurementLabel.numberOfLines = 0
measurementLabel.lineBreakMode = .byWordWrapping
measurementLabel.translatesAutoresizingMaskIntoConstraints = false
measurementLabel.widthAnchor.constraint(equalToConstant: width).isActive = true
let size = measurementLabel.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
return size
}
}
Note: the above is in Swift 3 syntax.
Then I implement the header size method of UICollectionViewDelegateFlowLayout
as:
extension MyCollectionViewController : UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
let text = textForHeader(inSection: section)
var size = UILabel.size(withAttributedText: text, forWidth: collectionView.frame.size.width)
size.height = size.height + 16
return size
}
}
The work of calculating the header size is delegated to the above UILabel
extension. The +16
is a experimentally derived fixed offset (8 + 8) that is based on margins and could be obtained programmatically.
All that's needed in the header callback is just to set the text:
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionElementKindSectionHeader, let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerIdentifier, for: indexPath) as? MyCollectionHeader {
let text = textForHeader(inSection: section)
headerView.label.text = text
return headerView
}
return UICollectionReusableView()
}
Starting from Swift 4 referenceSizeForHeaderInSection requires @objc attribute
@objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize{
//calculate size
return size
}
I had luck using Vladimir's method, but I had to set the frame of the template view to have equal width to my collection view.
templateHeader.bounds = CGRectMake(templateHeader.bounds.minX, templateHeader.bounds.minY, self.collectionView.bounds.width, templateHeader.bounds.height)
Additionally, my view has several resizable components, and having a template view seems robust enough to deal with any changes. Still feels like there should be an easier way.