UICollectionView header dynamic height using Auto Layout

后端 未结 5 1057
臣服心动
臣服心动 2020-11-30 02:45

I have a UICollectionView with a header of type UICollectionReusableView.

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

相关标签:
5条回答
  • 2020-11-30 02:53

    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.

    0 讨论(0)
  • 2020-11-30 03:03

    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() 
        }
    }
    
    0 讨论(0)
  • 2020-11-30 03:10

    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

    0 讨论(0)
  • 2020-11-30 03:15

    @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
    let size headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
                        return size
    return size
    
    0 讨论(0)
  • 2020-11-30 03:17

    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.

    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)
          }
      
      }
      
    0 讨论(0)
提交回复
热议问题