ios 使用自动布局的 UICollectionView 标题动态高度

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/39825290/
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 10:26:27  来源:igfitidea点击:

UICollectionView header dynamic height using Auto Layout

iosswiftuicollectionviewautolayout

提问by joda

I have a UICollectionViewwith a header of type UICollectionReusableView.

我有一个UICollectionView类型为 的标题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

结果

回答by Pim

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.

正如@Caio 在评论中注意到的那样,此解决方案将导致 iOS 10 及更早版本崩溃。在我的项目中,我通过包装上面的代码if #available(iOS 11.0, *) { ... }并在 else 子句中提供固定大小来“解决”这个问题。这并不理想,但在我的情况下是可以接受的。

回答by Joe Susnick

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)
        }
    
    }
    
  1. 在视图中嵌入标签。这将有助于自动调整大小。 在此处输入图片说明

  2. 确保标签上的约束是有限的。例如:从底部标签到可重用视图的大于约束将不起作用。见上图。

  3. 为您嵌入标签的视图添加一个出口到您的子类

    class CustomHeader: UICollectionReusableView {
        @IBOutlet weak var contentView: UIView!
    }
    
  4. 使初始布局无效

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
    
        collectionView.collectionViewLayout.invalidateLayout()
    }
    
  5. 布置标题以获得正确的大小

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

回答by Mohammad Razipour

Swift 3

斯威夫特 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

https://developer.apple.com/reference/uikit/uicollectionviewdelegateflowlayout/1617702-collectionview

回答by Ahmad F

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:

习俗UICollectionReusableView

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

    override func awakeFromNib() {
        super.awakeFromNib()

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

回答by Bartosz Kunat

@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:

@Pim 的回答对我有用,但正如评论部分中的 @linus_hologram 所提到的,这个解决方案让 AutoLayout 抱怨无法满足的约束。我找到了一个简单的解决方法:

In collectionView(_:layout:referenceSizeForHeaderInSection:)instead of getting a reference to the view that your instance of UICollectionViewwants 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 AutoLayoutwill be able to calculate your header's width:

collectionView(_:layout:referenceSizeForHeaderInSection:)不是获取对您的实例UICollectionView想要重用的视图的引用,collectionView(_:viewForSupplementaryElementOfKind:at:)只需动态创建标题视图类的实例并添加宽度约束,以便AutoLayout能够计算标题的宽度:

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