UICollectionView header dynamic height using Auto Layout

50,656

Solution 1

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.

Solution 2

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)
        }
    
    }
    

Solution 3

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

Solution 4

You could achieve it by implementing the following:

The ViewController:

class ViewController: UIViewController {
    @IBOutlet weak var collectionView: UICollectionView!

    // the text that you want to add it to the headerView's label:
    fileprivate let myText = "Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda."
}

extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        switch kind {
        case UICollectionElementKindSectionHeader:
            let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind,
                                                                             withReuseIdentifier: "customHeader",
                                                                             for: indexPath) as! CustomCollectionReusableView

            headerView.lblTitle.text = myText
            headerView.backgroundColor = UIColor.lightGray

            return headerView
        default:
            fatalError("This should never happen!!")
        }
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 100
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "customCell", for: indexPath)

        cell.backgroundColor = UIColor.brown
        // ...

        return cell
    }
}

extension ViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        // ofSize should be the same size of the headerView's label size:
        return CGSize(width: collectionView.frame.size.width, height: myText.heightWithConstrainedWidth(font: UIFont.systemFont(ofSize: 17)))
    }
}


extension String {
    func heightWithConstrainedWidth(font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: UIScreen.main.bounds.width, height: CGFloat.greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)

        return boundingBox.height
    }
}

The custom UICollectionReusableView:

class CustomCollectionReusableView: UICollectionReusableView {
    @IBOutlet weak var lblTitle: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()

        // setup "lblTitle":
        lblTitle.numberOfLines = 0
        lblTitle.lineBreakMode = .byWordWrapping
        lblTitle.sizeToFit() 
    }
}

Solution 5

@Pim's answer worked for me but as @linus_hologram in the comment section mentioned this solution makes AutoLayout complain about unsatisfiable constraints. I found a simple workaround:

In collectionView(_:layout:referenceSizeForHeaderInSection:) instead of getting a reference to the view that your instance of UICollectionView wants to reuse using collectionView(_:viewForSupplementaryElementOfKind:at:) just create an instance of your header view class on the fly and add a width constraint so that AutoLayout will be able to calculate your header's width:

let headerView = YourCustomHeaderClass() 
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.widthAnchor.constraint(equalToConstant: collectionView.frame.width).isActive = true
return headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
Share:
50,656

Related videos on Youtube

joda
Author by

joda

Updated on July 09, 2022

Comments

  • joda
    joda almost 2 years

    I have a UICollectionView with a header of type UICollectionReusableView.

    In it, I have a label whose length varies by user input.

    I'm looking for a way to have the header dynamically resize depending on the height of the label, as well as other subviews in the header.

    This is my storyboard:

    storyboard

    This the result when I run the app:

    result

    • joda
      joda over 7 years
      maybe this is work, but I can't understand objective-c, please I need it in swift
    • joda
      joda over 7 years
    • Ahmad F
      Ahmad F over 7 years
      probably, the question is duplicated: stackoverflow.com/questions/25642493/…
    • joda
      joda over 7 years
      @Ahmad the first solution not working with me, the second I don't know how I create a nib of header to try it, if you have any idea about it please I need your help
  • Joe Susnick
    Joe Susnick over 6 years
    Realized this won't necessarily work with longer lists since the header won't be visible when returning to that list. For a shorter list this will still work.
  • Julius Markūnas
    Julius Markūnas almost 6 years
    I made this work by having a reference to header view in controller. Than you!
  • BollMose
    BollMose over 4 years
    It's a solution, but there is a warning for UICollectionView, either dequeueReusableSupplementaryView or dequeueReusableCell to calculate the SupplementaryView/Cell's size will get that warning.
  • esbenr
    esbenr over 4 years
    This solution is not using dynamic height and autolayout.
  • Peter Lapisu
    Peter Lapisu over 4 years
    best answer, but note that the collectionView.frame.width for systemLayoutSizeFitting may be smaller due to insets and margins... you can be more precise by using: let bounds = collectionView.bounds let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout let width = bounds.size.width - layout.sectionInset.left - layout.sectionInset.right - layoutMargins.left - layoutMargins.right
  • linus_hologram
    linus_hologram over 4 years
    Your solution makes my cell resize correctly, however I get a huge amount of NSLayoutUnsatisfiableConstraint errors. The same cell is used as a "normal" content cell in another collection view, and there, it resizes correctly. I've used this tutorial to implement the dynamic cell resizing: medium.com/@andrea.toso/… Again, it works for the regular cells in another collection view, but not in this one, if I'm using it as a header cell. Do you have any Idea why?
  • Caio
    Caio about 4 years
    Please notice that it WONT WORK on iOS 9/10. Due to the fact that you can't dequeue a supplementary view BEFORE the collectionView(collectionView:kind:at) gets called by the delegate. It will crash.
  • andrea
    andrea almost 4 years
    To make this work for me, with longer lists that make the header not visible when scrolling I had to store the header as a class attribute (at the time of dequeueing), then inside the above method did: if let header = self.header.
  • Vyachaslav Gerchicov
    Vyachaslav Gerchicov almost 4 years
    doesn't work at all because this method is even not called
  • podkovyr
    podkovyr almost 4 years
    It seems this is the right way, however, there's an insidious bug in the method call: viewForSupplementaryElementOfKind Its implementation calls dequeueReusableSupplementaryView() which leads to creating another instance of the headerView. Since referenceSizeForHeaderInSection is called quite often there's a lot of headerViews will be created and moreover there will be visual bugs if the flow layout is used with sectionHeadersPinToVisibleBounds set to true. As a solution, the header view can be a lazy property instead: lazy var headerViewToMeasureHeight: UIView = { ... }()
  • famfamfam
    famfamfam almost 3 years
    @AndrewPo, it's work. but did u handle the orientation change problem? When change to lanscape the header gone
  • Zaporozhchenko Oleksandr
    Zaporozhchenko Oleksandr over 2 years
    it doesn't work for me, as view aren't created at reference method call, datasource method called later
  • Zaporozhchenko Oleksandr
    Zaporozhchenko Oleksandr over 2 years
    this method called before headers created
  • Zaporozhchenko Oleksandr
    Zaporozhchenko Oleksandr over 2 years
    how is this an answer? what is a self.mylabel in here? any explanation?