UICollectionView header dynamic height using Auto Layout

Here's an elegant, up to date solution.

As stated by others, first make sure that all you have constraints running from the very top of your header view to the top of the first subview, from the bottom of the first subview to the top of the second subview, etc, and from the bottom of the last subview to the bottom of your header view. Only then auto layout can know how to resize your view.

The following code snipped returns the calculated size of your header view.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {

    // Get the view for the first header
    let indexPath = IndexPath(row: 0, section: section)
    let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath)

    // Use this view to calculate the optimal size based on the collection view's width
    return headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
                                              withHorizontalFittingPriority: .required, // Width is fixed
                                              verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
}

Edit

As @Caio noticed in the comments, this solution will cause a crash on iOS 10 and older. In my project, I've "solved" this by wrapping the code above in if #available(iOS 11.0, *) { ... } and providing a fixed size in the else clause. That's not ideal, but acceptable in my case.


This drove me absolutely crazy for about half a day. Here's what finally worked.

Make sure the labels in your header are set to be dynamically sizing, one line and wrapping

  1. Embed your labels in a view. This will help with autosizing. enter image description here

  2. Make sure the constraints on your labels are finite. ex: A greater-than constraint from the bottom label to the reusable view will not work. See image above.

  3. Add an outlet to your subclass for the view you embedded your labels in

    class CustomHeader: UICollectionReusableView {
        @IBOutlet weak var contentView: UIView!
    }
    
  4. Invalidate the initial layout

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
    
        collectionView.collectionViewLayout.invalidateLayout()
    }
    
  5. Lay out the header to get the right size

    extension YourViewController: UICollectionViewDelegate {
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
    
            if let headerView = collectionView.visibleSupplementaryViews(ofKind: UICollectionElementKindSectionHeader).first as? CustomHeader {
                // Layout to get the right dimensions
                headerView.layoutIfNeeded()
    
                // Automagically get the right height
                let height = headerView.contentView.systemLayoutSizeFitting(UILayoutFittingExpandedSize).height
    
                // return the correct size
                return CGSize(width: collectionView.frame.width, height: height)
            }
    
            // You need this because this delegate method will run at least 
            // once before the header is available for sizing. 
            // Returning zero will stop the delegate from trying to get a supplementary view
            return CGSize(width: 1, height: 1)
        }
    
    }
    

Swift 3

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize{
    return CGSize(width: self.myCollectionView.bounds.width, height: self.mylabel.bounds.height)
}

https://developer.apple.com/reference/uikit/uicollectionviewdelegateflowlayout/1617702-collectionview