【问题标题】:How to set UICollectionViewCell height based on tallest UILabel如何根据最高的 UILabel 设置 UICollectionViewCell 高度
【发布时间】:2021-09-02 21:53:57
【问题描述】:

我想根据每个单元格内最高的UILabel 设置我的UICollectionViewCellUICollectionView 的高度。

为此,这是我的尝试:

import UIKit

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource,UICollectionViewDelegateFlowLayout {
    
    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var collectionViewLayout: UICollectionViewFlowLayout!
    
    @IBOutlet weak var collectionViewHeight: NSLayoutConstraint!
    let datasource = ["USA", "MGA USA USA USA USA", "FRA", "GER", "MGA", "FRA", "GER", "MGA", "FRA", "GER", "MGA", "FRA", "GER USA USA USA USA  USA USA", "MGA", "FRA", "GER", "MGA", "GER USA USA USA USA  USA USA GER USA USA USA USA  USA USA GER USA USA USA USA  USA USA GER USA USA USA USA  USA USA", "GER",]
    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.dataSource = self;
        collectionView.delegate = self;
        let nib = UINib.init(nibName: "CollectionViewCell", bundle: nil)
        collectionView.register(nib, forCellWithReuseIdentifier: "Cell")
        collectionViewLayout.minimumInteritemSpacing = 10000.0;
    }
    
//    override func viewDidLayoutSubviews() {
//        super.viewDidLayoutSubviews()
//        let height = collectionView.collectionViewLayout.collectionViewContentSize.height
//        collectionViewHeight.constant = height
//        self.view.layoutIfNeeded()
//    }
    
    var maxHeight = 0
    var indexPath: IndexPath?
    func newMax(){
        
        collectionViewHeight.constant = CGFloat(maxHeight+60)
        collectionView.collectionViewLayout.invalidateLayout()
        
        if let index = indexPath {
            collectionView.reloadItems(at: [index])
//            collectionView.reloadSections(IndexSet(integer: 0))
        }
        print("collectionViewHeight.constant \(collectionViewHeight.constant)")
    }
    
    func checkMax(height: Int, forItemAt indexPath: IndexPath ){
        
        var newMax = height > maxHeight ? height: maxHeight
        if(newMax != maxHeight) {
            maxHeight = newMax
            self.newMax()
            self.indexPath = indexPath
            print("max is \(maxHeight)")
        }
    }
    override func viewDidAppear(_ animated: Bool) {
        guard let cells = collectionView.visibleCells as? Array<CollectionViewCell> else {
            return
        }
        
        for cell in cells {
            let height = cell.countryNameLabel.bounds.size.height
            print(cell.countryNameLabel.text)
            print(height)
            checkMax(height: Int(height), forItemAt: IndexPath(index: 0))
        }
    }

    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        if let cell = cell as? CollectionViewCell {
            let height = cell.countryNameLabel.bounds.size.height
            print(cell.countryNameLabel.text)
            print(height)
            
            checkMax(height: Int(height), forItemAt: indexPath)
        } else {
            print(cell.bounds.size.height)
        }
    }
     func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        sizeForItemAt indexPath: IndexPath) -> CGSize {
//            let h =  maxHeight == 0 ?  123: maxHeight
        let h = collectionViewHeight.constant - 30
        print("setting cell size to \(h)")
        return CGSize(width: 123, height: h)
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        datasource.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cellTemp = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? CollectionViewCell
        print("cellTemp \(cellTemp)")
        let cell = cellTemp ?? CollectionViewCell()
        cell.setup(with: datasource[indexPath.row])
        return cell
    }
    
}

这是输出:

正如您在此 GIF 中看到的那样,我无法理解为什么 UICollectionViewCell 会在 scroll 上随机消失。我尝试了很多在 StackOverflow 上找到的东西,但我不走运。这就是我在这里问这个问题的原因。

如果我的整个方法是错误的并且有更好的方法,请提出建议。我也尝试了很多我在这里找到的东西,但这是唯一几乎可行的方法

【问题讨论】:

    标签: ios swift uitableview uicollectionview uicollectionviewcell


    【解决方案1】:

    注意事项

    1. 我在没有自动布局的情况下进行了尝试,因此如果您将其与自动布局一起使用,则需要添加必要的约束。
    2. 虽然这似乎适用于这个特定的用例,但您肯定需要根据您的用例调整这个想法。

    问题的根源可能是什么?

    newMax() 里面的代码调用 -

    1. collectionView.collectionViewLayout.invalidateLayout()
    2. collectionView.reloadItems(at: [index])

    newMax() 本身的触发方式如下 - willDisplay cell > checkMax() > newMax()

    这意味着您可能willDisplay cell 内过于频繁地调用invalidateLayout()reloadItems


    不调用invalidateLayout()怎么解决?

    1. collectionView 布局有一个固定的 itemSize。
    2. 允许单元格的 contentView 和标签在单元格外部(垂直)绘制。
    3. 至少尝试一下应该不会有什么坏处。

    CountryNameCollectionViewCell
    class CountryNameCollectionViewCell: UICollectionViewCell {
        
        static let cellIdentifier = String(describing: self)
        static let preferredWidth: CGFloat = 120
        
        var preferredWidth: CGFloat { CountryNameCollectionViewCell.preferredWidth }
        var labelHeight: CGFloat { titleLabel.bounds.height }
        
        lazy var titleLabel: UILabel = {
            let label = UILabel(frame: CGRect(x: 0, y: 0, width: preferredWidth, height: 0))
            label.backgroundColor = .yellow
            label.font = .systemFont(ofSize: 32)
            label.textColor = .black
            label.numberOfLines = 0
            
            self.contentView.addSubview(label)
            return label
        }()
        
        func populate(title: String) {
            titleLabel.text = title
            
            let updatedHeight = titleLabel.sizeThatFits(
                CGSize(width: preferredWidth, height: .greatestFiniteMagnitude)
            ).height
            
            // Hacky part
            // Needed height/size is applied to contentView as well as titleLabel
            // We have a guarantee from call site that -
            // collectionView will be as tall as we need based on this height
            let updatedSize = CGSize(width: preferredWidth, height: updatedHeight)
            let updatedFrame = CGRect(origin: .zero, size: updatedSize)
            self.contentView.frame = updatedFrame
            titleLabel.frame = updatedFrame
        }
        
    }
    

    TopAlignedFlowLayout
    /// Needed for making all the cells/items stick to top
    class TopAlignedFlowLayout: UICollectionViewFlowLayout {
        var topSpace: CGFloat = 20
        var attributesCache: [IndexPath: UICollectionViewLayoutAttributes] = [:]
        
        override func prepare() {
            super.prepare()
            self.attributesCache = [:]
            
            guard let cv = self.collectionView else { return }
            
            for s in 0..<cv.numberOfSections {
                for i in 0..<cv.numberOfItems(inSection: s) {
                    let ip = IndexPath(item: i, section: s)
                    
                    let attributes = super.layoutAttributesForItem(at: ip)
                    attributes?.frame.origin.y = topSpace
                    attributesCache[ip] = attributes
                }
            }
        }
        
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            let attributes = super.layoutAttributesForElements(in: rect)
            
            var updatedAttributes: [UICollectionViewLayoutAttributes] = []
            attributes?.forEach({ attr in
                let ip = attr.indexPath
                if let updatedAttr = attributesCache[ip] {
                    updatedAttributes.append(updatedAttr)
                }
            })
            
            return updatedAttributes
        }
        
        override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
            let attributes = super.layoutAttributesForItem(at: indexPath)
            
            var updatedAttributes = attributes
            if let ip = attributes?.indexPath, let updated = attributesCache[ip] {
                updatedAttributes = updated
            }
            
            return updatedAttributes
        }
    }
    

    视图控制器
    class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
        
        // MARK: - Instance Variables
        let minCollectionViewHeight: CGFloat = 80 // including top/bottom insets
        let cvOrigin = CGPoint(x: 0, y: 100)
        
        lazy var flowLayout: UICollectionViewFlowLayout = {
            let sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
            
            let layout = TopAlignedFlowLayout()
            layout.topSpace = sectionInset.top
            layout.scrollDirection = .horizontal
            layout.sectionInset = sectionInset
            
            layout.itemSize = CGSize(
                width: CountryNameCollectionViewCell.preferredWidth,
                height: minCollectionViewHeight - (sectionInset.top + sectionInset.bottom) - 2
            )
            
            layout.minimumInteritemSpacing = self.view.bounds.height
            layout.minimumLineSpacing = 20
            
            return layout
        }()
        
        lazy var countriesCollectionView: UICollectionView = {
            let size = CGSize(width: self.view.bounds.width, height: minCollectionViewHeight)
            let frame = CGRect(origin: cvOrigin, size: size)
            
            let cv = UICollectionView(frame: frame, collectionViewLayout: flowLayout)
            cv.autoresizingMask = .flexibleWidth
            cv.backgroundColor = .red
            cv.showsVerticalScrollIndicator = false
            
            cv.dataSource = self
            cv.delegate = self
            
            self.view.addSubview(cv)
            return cv
        }()
        
        let countries = [
            "USA",
            "MGA USA",
            "FRA USA USA",
            "GER USA USA USA",
            "MGA USA USA USA USA",
            "FRA USA USA USA USA USA",
            "GER USA USA USA USA USA USA",
            "MGA USA USA USA USA USA USA USA",
            "FRA USA USA USA USA USA USA USA USA",
            "GER USA USA USA USA USA USA USA USA USA",
            "MGA USA USA USA USA USA USA USA USA",
            "FRA",
            "GER USA USA USA USA  USA USA",
            "MGA USA USA",
            "FRA",
            "GER USA USA USA",
            "MGA",
            "GER USA USA USA USA  USA",
            "GER",
        ]
        
        
        // MARK: - View Life Cycle
        override func viewDidLoad() {
            super.viewDidLoad()
            
            countriesCollectionView.register(
                CountryNameCollectionViewCell.self,
                forCellWithReuseIdentifier: CountryNameCollectionViewCell.cellIdentifier
            )
        }
        
        
        // MARK: - UICollectionViewDataSource Methods
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            countries.count
        }
        
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CountryNameCollectionViewCell.cellIdentifier, for: indexPath) as! CountryNameCollectionViewCell
            cell.populate(title: countries[indexPath.item])
            return cell
        }
        
        
        // MARK: - UICollectionViewDelegate Methods
        func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
            self.updateCurrentHeight(for: cell, incoming: true)
        }
        
        func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
            self.updateCurrentHeight(for: cell, incoming: false)
        }
        
        
        // MARK: - Helpers
        var currentHeight: CGFloat = 0 {
            didSet {
                guard self.isViewLoaded else { return }
                
                heightUpdateWorkItem?.cancel()
                
                let workItem = DispatchWorkItem { self.updateCollectionViewHeight() }
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem)
                heightUpdateWorkItem = workItem
            }
        }
        var heightUpdateWorkItem: DispatchWorkItem?
        
        private func updateCurrentHeight(for cell: UICollectionViewCell, incoming: Bool) {
            var updatedHeight: CGFloat = 0
            let inset = flowLayout.sectionInset
            
            if incoming, let countryNameCell = cell as? CountryNameCollectionViewCell {
                let idealHeight = inset.top + countryNameCell.labelHeight + inset.bottom
                updatedHeight = max(minCollectionViewHeight, idealHeight)
            }
            else if var cells = countriesCollectionView.visibleCells as? [CountryNameCollectionViewCell] {
                cells.removeAll(where: { $0 === cell })
                let maxLabelHeight = cells.map({ $0.labelHeight }).max() ?? 0
                updatedHeight = inset.top + maxLabelHeight + inset.bottom
            }
            
            if updatedHeight > 0, updatedHeight != currentHeight {
                if (incoming && updatedHeight > currentHeight) ||
                    (!incoming && updatedHeight < currentHeight) {
                    currentHeight = updatedHeight
                }
            }
        }
        
        private func updateCollectionViewHeight() {
            countriesCollectionView.frame = CGRect(
                origin: cvOrigin,
                size: CGSize(width: countriesCollectionView.bounds.width, height: currentHeight)
            )
            print("currentHeight: \(currentHeight)")
        }
        
    }
    

    看起来怎么样?

    【讨论】:

      【解决方案2】:

      你可以通过计算collectionView中的标签高度来做到这一点。问题可能是在集合视图中填充数据之前我们需要一个标签高度。

      1. 所以再创建一个[CGFloat] 类型的数组。

      2. 使用此extension 存储所有标签的高度。

      3. 从数组中取出最大值并将其存储在单独的变量中。

      4. sizeForItemAt indexPath方法中使用这个变量。

        func calculateHeight() -> CGFloat?{
        let datasource = ["USA", "MGA USA USA USA USA", "FRA", "GER", "MGA", "FRA", "GER", "MGA", "FRA", "GER", "MGA", "FRA", "GER USA USA USA USA  USA USA", "MGA", "FRA", "GER", "MGA", "GER USA USA USA USA  USA USA GER USA USA USA USA  USA USA GER USA USA USA USA  USA USA GER USA USA USA USA  USA USA", "GER"]
        var arrayHeight: [CGFloat] = []
        for data in datasource
        {
            let label = UILabel.init(frame: CGRect.init(x: 0, y: 0, width: 50, height: 50))
            label.text = data
            arrayHeight.append(label.requiredHeight)
        }
        
        return arrayHeight.max()    }
        

        扩展UILabel{

        public var requiredHeight: CGFloat {
            let label = UILabel(frame: CGRect(x: 0, y: 0, width: frame.width, height: CGFloat.greatestFiniteMagnitude))
            label.numberOfLines = 0
            label.lineBreakMode = NSLineBreakMode.byWordWrapping
            label.font = font
            label.text = text
            label.attributedText = attributedText
            label.sizeToFit()
            return label.frame.height
        }}
        

      【讨论】:

        猜你喜欢
        • 2011-08-06
        • 1970-01-01
        • 1970-01-01
        • 2018-10-24
        • 2021-08-12
        • 1970-01-01
        • 2016-08-21
        • 2020-12-23
        • 2013-11-06
        相关资源
        最近更新 更多