【问题标题】:Dynamic UICollectionView header size based on UILabel基于 UILabel 的动态 UICollectionView 标头大小
【发布时间】:2014-10-27 20:18:42
【问题描述】:

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

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

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
}

文本的显示似乎正常,但高度计算似乎不起作用(见下面的屏幕截图)。另外,我认为我也不能通过 collectionView...referenceSizeForHeaderInSection 函数访问 UILabel。有关如何正确计算 CGSize 的任何建议?

【问题讨论】:

  • 我无法真正解决问题。但我确实找到了涉及 UX 工作流程更改的解决方法。我为标题定义了一个固定的标题高度和固定的大小。如果标题较长,我会截断文本,但允许用户单击它进入显示全文的模式视图。在 iOS 中,我总是发现很难为简单的问题实施解决方案。
  • @Dean 我想我必须做同样的事情,可能是在 UI 中的某处放置一个“阅读更多...”按钮......

标签: ios ios7 swift uicollectionview


【解决方案1】:

我就是这样做的:

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."]

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

我在我的 UICollectionReusableView 子类 (MyHeaderCollectionReusableView) 中有一个名为 label 的标签插座,我通过在情节提要中分配它来将其用于我的部分标题视图(将“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)
    }

【讨论】:

  • 这真的是最干净的解决方案吗?为什么是苹果……为什么?
  • 为什么不加载笔尖和计算高度工作:(
  • 自动布局之前的遗留问题。脏苹果
  • 我添加了an alternative answer,而不是手动重新创建 UILabel,我们将现有的 UILabel 从viewForSupplementaryElementOfKind:at: 中取出。
【解决方案2】:

像提问者一样,我有一个 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
    }
}

注意:以上是 Swift 3 语法。

然后我实现UICollectionViewDelegateFlowLayout的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
    }
}

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

在标头回调中只需要设置文本:

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

【讨论】:

    【解决方案3】:

    想法是在内存中有一个模板头实例,以在创建结果头视图之前计算所需的高度。您应该将部分标题视图移动到单独的 .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
      }
    
    }
    

    然后您将能够在流布局委托方法中计算标题大小(在我的示例中为高度):

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

    然后像往常一样创建并返回您的常规标题视图,因为您已经在流布局委托方法中计算了它的大小:

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

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

    【讨论】:

      【解决方案4】:

      我们可以简单地使用现有的标签来计算拟合大小,而不是从代码中重新创建一个新标签,如多个答案所示。

      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
      }
      

      适用于 iOS 11+。

      【讨论】:

        【解决方案5】:

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

            templateHeader.bounds = CGRectMake(templateHeader.bounds.minX, templateHeader.bounds.minY, self.collectionView.bounds.width, templateHeader.bounds.height)
        

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

        【讨论】:

          【解决方案6】:

          在您的单元格中添加以下内容:

          fileprivate static let font = UIFont(name: FontName, size: 16)
          fileprivate static let insets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
          

          相应地调整字体和插图。

          然后将以下内容添加到您的单元格中

             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
          }
          

          您现在可以使用此功能计算自动高度

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

          【讨论】:

            【解决方案7】:

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

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

            【讨论】:

            • 不,它没有。对我来说,它仍然在没有@objc 的情况下调用
            【解决方案8】:

            你必须实现UICollectionViewDelegate方法referenceSizeForHeaderInSection

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

            【讨论】:

            • 我想我已经实现了referenceSizeForHeaderInSection,它是列出的第一个函数。我调试了更多,但边界 Rect 计算不正确。我也尝试过添加段落换行词属性,但没有奏效。
            • 抱歉,您没有格式化代码以使其可见。也许你应该改变你的问题的误导性标题并将标签放在外面?
            • 嗨@Mundi 联系您的最佳方式是什么?想问一个简单的问题。
            • 只要提出问题并在此处发布链接。
            猜你喜欢
            • 2020-07-18
            • 1970-01-01
            • 2017-01-05
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-07-08
            相关资源
            最近更新 更多