ios 基于 UILabel 的动态 UICollectionView 标头大小

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/25642493/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 02:11:04  来源:igfitidea点击:

Dynamic UICollectionView header size based on UILabel

iosios7swiftuicollectionview

提问by Dean

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 based on the height of UILabel. The UILabel has lines = 0.

我已经阅读了很多关于向 UICollectionView 添加标题的帖子。在 Swift 的 iOS 7+ 应用程序中,我试图添加一个带有 UILabel 的标题,其高度应根据 UILabel 的高度进行调整。UILabel 的行数 = 0。

I've set up the header in IB with AutoLayout

我已经使用 AutoLayout 在 IB 中设置了标题

enter image description here

在此处输入图片说明

The ViewController implements UICollectionViewDelegate, UICollectionViewDataSource. I didn't set up a custom class for the header but am using these two functions:

ViewController 实现UICollectionViewDelegate, UICollectionViewDataSource. 我没有为标题设置自定义类,但正在使用这两个函数:

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
      //description is a String variable defined in the class
    let size:CGSize = (description as NSString).boundingRectWithSize(CGSizeMake(CGRectGetWidth(collectionView.bounds) - 20.0, 180.0), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName: UIFont(name: "Helvetica Neue", size: 16.0)], context: nil).size
    return CGSizeMake(CGRectGetWidth(collectionView.bounds), ceil(size.height))
}

func collectionView(collectionView: UICollectionView!, viewForSupplementaryElementOfKind kind: String!, atIndexPath indexPath: NSIndexPath!) -> UICollectionReusableView! {
    var reusableview:UICollectionReusableView = UICollectionReusableView()
    if (kind == UICollectionElementKindSectionHeader) {
                    //listCollectionView is an @IBOutlet UICollectionView defined at class level, using collectionView crashes
            reusableview = listCollectionView.dequeueReusableSupplementaryViewOfKind(UICollectionElementKindSectionHeader, withReuseIdentifier: "ListHeader", forIndexPath: indexPath) as UICollectionReusableView
            let label = reusableview.viewWithTag(200) as UILabel  //the UILabel within the header is tagged with 200
            label.text = description   //description is a String variable defined in the class
        }
    }
    return reusableview
}

The displaying of the text seems to be working but the height calculation doesn't seem to be working (see screenshot below). Also, I don't think I can access the UILabel via the collectionView...referenceSizeForHeaderInSectionfunction either. Any suggestions on how to calculate CGSize correctly?

文本的显示似乎有效,但高度计算似乎无效(请参见下面的屏幕截图)。另外,我认为我也无法通过该collectionView...referenceSizeForHeaderInSection函数访问 UILabel 。关于如何正确计算 CGSize 的任何建议?

enter image description here

在此处输入图片说明

回答by Alvaro

This is how I did it:

我是这样做的:

let labels = [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ac lorem enim. Curabitur rhoncus efficitur quam, et pretium ipsum. Nam eu magna at velit sollicitudin fringilla nec nec nisi. Quisque nec enim et ipsum feugiat pretium. Vestibulum hendrerit arcu ut ipsum gravida, ut tincidunt justo pellentesque. Etiam lacus ligula, aliquet at lorem vel, ullamcorper commodo turpis. Nullam commodo sollicitudin mauris eu faucibus.",
"Lorem ipsum dolor",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ac lorem enim. Curabitur rhoncus efficitur quam, et pretium ipsum. Nam eu magna at velit sollicitudin fringilla nec nec nisi. Quisque nec enim et ipsum feugiat pretium."]

The basic idea is to create an identical UILabelto the one that will be shown in the section header. That label will be used to set the desired size for the header in the referenceSizeForHeaderInSectionmethod.

基本思想是创建一个与UILabel将在节标题中显示的相同的内容。该标签将用于为referenceSizeForHeaderInSection方法中的标题设置所需的大小。

I have a label outlet called labelin my UICollectionReusableViewsubclass (MyHeaderCollectionReusableView), which I use for my section header view by assigning it in the storyboard (setting "MyHeader" as Reuse Identifier for the section view). That mentioned label has the horizontal and vertical space constraints to the section header borders in order to autolayout correctly.

我有一个label在我的UICollectionReusableView子类 ( MyHeaderCollectionReusableView) 中调用的标签出口,我通过在故事板中分配它(将“MyHeader”设置为剖面视图的重用标识符)来将其用于我的剖面标题视图。提到的标签对节标题边框具有水平和垂直空间约束,以便正确自动布局。

override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 3
    }

override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {

        let headerView =
        collectionView.dequeueReusableSupplementaryViewOfKind(kind,
            withReuseIdentifier: "MyHeader",
            forIndexPath: indexPath)
            as MyHeaderCollectionReusableView

        headerView.label.text = labels[indexPath.section]

        return headerView

    }

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        // that -16 is because I have 8px for left and right spacing constraints for the label.
        let label:UILabel = UILabel(frame: CGRectMake(0, 0, collectionView.frame.width - 16, CGFloat.max))
        label.numberOfLines = 0
        label.lineBreakMode = NSLineBreakMode.ByWordWrapping
       //here, be sure you set the font type and size that matches the one set in the storyboard label
        label.font = UIFont(name: "Helvetica", size: 17.0)
        label.text = labels[section]
        label.sizeToFit()

// Set some extra pixels for height due to the margins of the header section.  
//This value should be the sum of the vertical spacing you set in the autolayout constraints for the label. + 16 worked for me as I have 8px for top and bottom constraints.
        return CGSize(width: collectionView.frame.width, height: label.frame.height + 16)
    }

回答by Nick Ager

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 UILabelto measure the height of a multiline label with a known width:

像提问者一样,我有一个 UICollectionView,它包含一个带有单个标签的标题,我想改变它的高度。我创建了一个扩展来UILabel测量具有已知宽度的多行标签的高度:

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.

注意:以上是 Swift 3 语法。

Then I implement the header size method of UICollectionViewDelegateFlowLayoutas:

然后我实现了UICollectionViewDelegateFlowLayoutas的 header size 方法:

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 UILabelextension. The +16is a experimentally derived fixed offset (8 + 8) that is based on margins and could be obtained programmatically.

计算标头大小的工作委托给上述UILabel扩展。这+16是一个实验得出的固定偏移量 (8 + 8),它基于边距,可以通过编程方式获得。

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

回答by Vladimir Afinello

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:

这个想法是在内存中有一个模板头实例来在创建结果头视图之前计算所需的高度。您应该将部分标题视图移动到单独的 .nib 文件,设置所有自动布局约束并在 viewDidLoad 方法中实例化模板,如下所示:

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

忘了说一个重要的时刻——你的标题视图应该在它的 contentView 上有一个自动布局约束,以适应集合视图的宽度(加上或减去所需的边距)。

回答by C?ur

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:

您的代码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+.

适用于 iOS 11+。

回答by Mohamed Ali

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

回答by Edu

Starting from Swift 4 referenceSizeForHeaderInSection requires @objc attribute

从 Swift 4 开始,referenceSizeForHeaderInSection 需要 @objc 属性

@objc func collectionView(_ collectionView: UICollectionView, layout  collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize{
    //calculate size 
    return size
}

回答by Jacob Ruth

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.

我很幸运使用 Vladimir 的方法,但我必须将模板视图的框架设置为与我的集合视图具有相同的宽度。

    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.

此外,我的视图有几个可调整大小的组件,并且具有模板视图似乎足够强大以应对任何更改。仍然觉得应该有更简单的方法。

回答by Mundi

You have to implement the UICollectionViewDelegatemethod referenceSizeForHeaderInSection.

你必须实现UICollectionViewDelegate方法referenceSizeForHeaderInSection

There you have to calculate the height without using the label by calling boundingRectWithSize:options:context:on the string with the appropriate attributes.

在那里,您必须通过调用boundingRectWithSize:options:context:具有适当属性的字符串来计算高度而不使用标签。