UITableViewCell 内的 UICollectionView 自动计算高度的问题

问题描述 投票:0回答:1

好?

我在实现 UITableViewCell 中的 UICollectionview 时遇到问题,因为该组件的高度计算无法正常工作,即使使用

updateConstraints()
并由委托
tableView.beginUpdates()
调用,有时也能正常工作,有时会渲染缺失的高度。

正确渲染 correctly render

渲染错误 wrong render

我的布局是一个包含很多单元格的表格,这些单元格在两个集合视图中有一些文本和项目(我使用 UICollectionView 因为这些项目的大小需要动态)

UITableViewCell的实现:

protocol CardTableViewCellDelegate: AnyObject {
    func updateTableView()
}

class CardTableViewCell: UITableViewCell {
    static let identifier: String = "CardTableViewCell"
    
    lazy var cardView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.layer.cornerRadius = 8
        view.layer.masksToBounds = true
        view.clipsToBounds = true
        view.layer.borderWidth = 1
        view.layer.borderColor = UIColor.separator.cgColor
        
        return view
    }()
    
    lazy var containerStackView: UIStackView = {
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.spacing = 8
        
        return stackView
    }()
    
    lazy var title: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .preferredFont(forTextStyle: .headline)
        label.numberOfLines = 0
        
        return label
    }()
    
    lazy var subtitle: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .preferredFont(forTextStyle: .subheadline)
        label.numberOfLines = 0
        
        return label
    }()
    
    lazy var sourceNew: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .preferredFont(forTextStyle: .footnote)
        label.numberOfLines = 0
        
        return label
    }()
    
    var assetsCollectionView: AssetsCollectionView = {
        let collectionView = AssetsCollectionView()
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        
        return collectionView
    }()
    
    lazy var benchmarksCollectionView: BenchmarksCollectionView = {
        let collectionView = BenchmarksCollectionView()
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        
        return collectionView
    }()

    weak var delegate: CardTableViewCellDelegate?
    
    // MARK: - Init
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Methods
    func configure(with card: Card) {
        title.text = card.title
        subtitle.text = card.subtitle
        sourceNew.text = card.sourceNew
        
        
        configureAssetsCollectionView(with: card.assets)
        configurebenchmarksCollectionView(with: card.benchmarks)
    }
    
    private func configureAssetsCollectionView(with model: [Asset]?) {
        if let assets = model {
            assetsCollectionView.configure(with: assets)
            
            containerStackView.addArrangedSubview(assetsCollectionView)
            
            assetsCollectionView.updateConstraints()
            delegate?.updateTableView()
        }
    }
    
    private func configurebenchmarksCollectionView(with model: [Benchmark]?) {
        if let benchmarks = model {
            benchmarksCollectionView.configure(with: benchmarks)
            
            containerStackView.addArrangedSubview(benchmarksCollectionView)
        }
    }
}

// MARK: - ViewCode
extension CardTableViewCell {
    func setupView() {
        setupLayout()
        setupHierarchy()
        setupConstrains()
    }
    
    func setupLayout() {
        backgroundColor = .white
        cardView.backgroundColor = .clear
    }
    
    func setupHierarchy() {
        [title,
         subtitle,
         sourceNew].forEach(containerStackView.addArrangedSubview(_:))
        
        cardView.addSubview(containerStackView)
        
        contentView.addSubview(cardView)
    }
    
    func setupConstrains() {
        NSLayoutConstraint.activate([
            cardView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
            cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16),
            
            containerStackView.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 8),
            containerStackView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 8),
            containerStackView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -8),
            containerStackView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -8),
            
            assetsCollectionView.heightAnchor.constraint(greaterThanOrEqualToConstant: 22),
            benchmarksCollectionView.heightAnchor.constraint(greaterThanOrEqualToConstant: 22)
        ])
    }
}

以及 UICollectionView:

import UIKit

class AssetsCollectionView: UICollectionView {
    private var assets: [Asset] = []
    
    override var intrinsicContentSize: CGSize {
        self.layoutIfNeeded()
        print(contentSize)
        return self.contentSize
    }
    
    init() {
        let layout = LeftAlignedCollectionViewFlowLayout()
        layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        layout.minimumLineSpacing = 8
        layout.minimumInteritemSpacing = 8
        super.init(frame: .zero, collectionViewLayout: layout)
        
        delegate = self
        dataSource = self
        
        register(AssetCollectionViewCell.self,
                 forCellWithReuseIdentifier: AssetCollectionViewCell.identifier)
        
        showsHorizontalScrollIndicator = false
        showsVerticalScrollIndicator = false
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func configure(with assets: [Asset]) {
        self.assets = assets
        reloadData()
    }
}

extension AssetsCollectionView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return assets.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView
            .dequeueReusableCell(withReuseIdentifier: AssetCollectionViewCell.identifier, for: indexPath) as? AssetCollectionViewCell else {
            return UICollectionViewCell()
        }
        
        cell.configure(with: assets[indexPath.row])
        
        return cell
    }
}

那么,这个组件可以在不使用身高计算技巧的情况下自动计算你的身高吗?我尝试使用很多方法发送单元格来重新计算您的约束,但没有人起作用。 :(

我知道我可以使用项目的数量来计算高度,但我需要更强大的代码来处理可能的原因...任何人都可以帮助我吗?

本地重现的 github url:https://github.com/ramonfsk/MarketContextTimeline.git

ios swift uitableview uicollectionview autolayout
1个回答
0
投票

尝试使用“自调整大小”集合视图时,您会遇到许多问题。

集合视图旨在基于框架布局单元格 - 而不是相反。

如果我们查看您的

LeftAlignedCollectionViewFlowLayout
(无论是否使用子类布局都会发生这种情况):

`layoutAttributesForElements(in rect: CGRect)`

被称为。

如果我们开始布局:

assetsCollectionView.heightAnchor.constraint(greaterThanOrEqualToConstant: 22)

rect
中的
layoutAttributesForElements(in rect: CGRect)
将是:

(0.0, 0.0, 353.0, 22.0)  // the width will vary

假设我们正在使用这些字符串:

        "MGLU3", "IBVV11", "BBSA3", "Slightly Longer String",
        "FIVE", "SIX", "SEVEN", "EIGHT",
        "NINE", "TEN", "ELEVEN", "TWELVE",
        "Hash Index Ethereum position replicated with a 100% accuracy",
        "FOURTEEN", "FIFTEEN", "SIXTEEN", "SEVENTEEN",

目标是这样的:

goal

布局开始看起来像这样:

step1

让我们添加一个显示初始帧的红色矩形:

step2

和一个绿色括号,显示 当前

collectionView.contentSize.height
:

step3

因此,让我们展开集合视图框架以显示所有单元格:

step4

请注意,

contentSize.height
确实不是反映了所有单元格...并且它可能会根据实际单元格大小、初始帧等而有很大变化。我们没有得到有效的(出于我们的目的)
 contentSize.height
直到所有单元格 都已由集合视图布局。

即使我们跳过一堆圈子来“强制”渲染所有单元格,我们仍然存在计时问题......集合视图在知道其宽度之前无法布局单元格,并且因为它嵌入在对于表格视图单元格,我们必须进行“回调”以自动调整单元格高度。

此外——让我们使用您的代码,但显式设置集合视图的高度以显示所有单元格:

step5

看起来不错——除了单元格被重用...所以让我们上下滚动一下:

step6

是的,显然不可接受。

所以...我建议使用

UIView
子类来自行布局标签。

总体逻辑是:

  • 使用
    .systemLayoutSizeFitting(...)
    获取“项目”的大小
  • 如果它适合当前行,请设置其框架
  • 如果太宽,请向下移动一行
  • 如果它是一行中的第一项,并且仍然太宽,请允许其自动换行
  • 布置完所有项目后,更新高度约束,以便此视图在自动布局下表现良好

我们将从“填充标签视图”开始,它大致相当于一个单元格:

class PaddedLabelView: UIView {
    
    let theLabel = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {

        theLabel.translatesAutoresizingMaskIntoConstraints = false
        addSubview(theLabel)
        let edgeConstraints: [NSLayoutConstraint] = [
            theLabel.topAnchor.constraint(equalTo: topAnchor, constant: 2.0),
            theLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
            theLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
            theLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -2.0),
        ]
        // this prevents auto-layout complaints
        edgeConstraints[2].priority = .required - 1
        edgeConstraints[3].priority = .required - 1
        NSLayoutConstraint.activate(edgeConstraints)

        // properties
        theLabel.numberOfLines = 0
        theLabel.textColor = .white
        theLabel.font = .systemFont(ofSize: 12.0, weight: .bold)
        backgroundColor = .systemBlue
        layer.cornerRadius = 6.0
        layer.masksToBounds = true
    }
}

现在我们创建一个“排列视图”类:

class BasicArrangedViewsView: UIView {
    
    public var theStrings: [String] = [] {
        didSet {
            // remove existing views
            for v in labelViews { v.removeFromSuperview() }
            labelViews = []
            for str in theStrings {
                let t = PaddedLabelView()
                t.theLabel.text = str
                addSubview(t)
                labelViews.append(t)
            }
            calcFrames(bounds.width)
        }
    }
    
    // horizontal space between label views
    let interItemSpace: CGFloat = 8.0
    
    // vertical space between "rows"
    let lineSpace: CGFloat = 8.0
    
    // we use these to set the intrinsic content size
    private var myHeight: CGFloat = 0.0
    private var myWidth: CGFloat = 0.0
    
    private var myHC: NSLayoutConstraint!
    
    private var labelViews: [PaddedLabelView] = []
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        // initialize height constraint, but don't activate it yet
        myHC = heightAnchor.constraint(equalToConstant: 0.0)
        myHC.priority = .required - 1
    }
    
    func calcFrames(_ tagetWidth: CGFloat) {
        // this can be called multiple times, and
        //  may be called before we have a frame
        if tagetWidth == 0.0 {
            return
        }
        
        var newWidth: CGFloat = 0.0
        var newHeight: CGFloat = 0.0
        
        var x: CGFloat = 0.0
        var y: CGFloat = 0.0
        
        var isMultiLine: Bool = false
        var thisRect: CGRect = .zero
        
        for thisView in labelViews {
            // start with NOT needing to wrap
            isMultiLine = false
            // set the frame width to a very wide value, so we get the non-wrapped size
            thisView.frame.size.width = 5000
            thisView.layoutIfNeeded()
            var sz: CGSize = thisView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
            sz.width = ceil(sz.width)
            sz.height = ceil(sz.height)
            thisRect = .init(x: x, y: y, width: sz.width, height: sz.height)
            // if this item is too wide to fit on the "row"
            if thisRect.maxX > tagetWidth {
                // if this is not the FIRST item on the row
                //  move down a row and reset x
                if x > 0.0 {
                    x = 0.0
                    y = thisRect.maxY + lineSpace
                }
                thisRect = .init(x: x, y: y, width: sz.width, height: sz.height)
                // if this item is still too wide to fit, that means
                //  it needs to wrap the text
                if thisRect.maxX > tagetWidth {
                    isMultiLine = true
                    // this will give us the height based on max available width
                    sz = thisView.systemLayoutSizeFitting(.init(width: tagetWidth, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
                    sz.width = ceil(sz.width)
                    sz.height = ceil(sz.height)
                    // update the frame
                    thisView.frame.size = sz
                    thisView.layoutIfNeeded()
                    // this will give us the width needed for the wrapped text (instead of the max available width)
                    sz = thisView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
                    sz.width = ceil(sz.width)
                    sz.height = ceil(sz.height)
                    thisRect = .init(x: x, y: y, width: sz.width, height: sz.height)
                }
            }
            // if we needed to wrap the text, adjust the next Y and reset X
            if isMultiLine {
                x = 0.0
                y = thisRect.maxY + lineSpace
            }
            thisView.frame = thisRect
            // update the max width var
            newWidth = max(newWidth, thisRect.maxX)
            // if we did NOT need to wrap lines, adjust the X
            if !isMultiLine {
                x += sz.width + interItemSpace
            }
        }
        
        newHeight = thisRect.maxY
        
        if myWidth != newWidth || myHeight != newHeight {
            myWidth = newWidth
            myHeight = newHeight
            // don't activate the constraint if we're not in an auto-layout case
            if self.translatesAutoresizingMaskIntoConstraints == false {
                myHC.isActive = true
            }
            // update the height constraint constant
            myHC.constant = myHeight
            invalidateIntrinsicContentSize()
        }
    }
    
    override var intrinsicContentSize: CGSize {
        return .init(width: myWidth, height: myHeight)
    }
    override func invalidateIntrinsicContentSize() {
        super.invalidateIntrinsicContentSize()
        
        // walk-up the view hierarchy...
        // this will handle self-sizing cells in a table or collection view, without
        //  the need to "call back" to the controller
        var sv = superview
        while sv != nil {
            if sv is UITableViewCell || sv is UICollectionViewCell {
                sv?.invalidateIntrinsicContentSize()
                sv = nil
            } else {
                sv = sv?.superview
            }
        }
    }
    
    override var bounds: CGRect {
        willSet {
            if newValue.width != bounds.width {
                calcFrames(newValue.width)
            }
        }
    }
    
}

和一个示例控制器:

class BasicVC: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        
        // sample strings
        let strs: [String] = [
            "MGLU3", "IBVV11", "BBSA3", "Slightly Longer String",
            "FIVE", "SIX", "SEVEN", "EIGHT",
            "NINE", "TEN", "ELEVEN", "TWELVE",
            "Hash Index Ethereum position replicated with a 100% accuracy",
            "FOURTEEN", "FIFTEEN", "SIXTEEN", "SEVENTEEN",
        ]

        let aView = BasicArrangedViewsView()
        
        aView.theStrings = strs
        
        aView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(aView)

        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            aView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            aView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            aView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            // don't set a bottom or height constraint
        ])
        
        // so we can see the view frame
        aView.backgroundColor = .systemYellow
    }
    
}

结果:

step7

以及它在您的项目中的实现方式:

step8

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.