Dynamic UICollectionView header size based on UILabel

前端 未结 8 1927
梦谈多话
梦谈多话 2020-12-08 04:51

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

相关标签:
8条回答
  • 2020-12-08 05:34

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

    0 讨论(0)
  • 2020-12-08 05:39

    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)
    }
    
    0 讨论(0)
  • 2020-12-08 05:44

    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+.

    0 讨论(0)
  • 2020-12-08 05:46

    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()
    }
    
    0 讨论(0)
  • 2020-12-08 05:47

    Starting from Swift 4 referenceSizeForHeaderInSection requires @objc attribute

    @objc func collectionView(_ collectionView: UICollectionView, layout  collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize{
        //calculate size 
        return size
    }
    
    0 讨论(0)
  • 2020-12-08 05:49

    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.

    0 讨论(0)
提交回复
热议问题