如何让UICollectionView高度随内容动态变化?(含自定义TagView场景)
解决UICollectionView可变宽度单元格及动态高度问题
这个问题我之前也碰到过,你遇到的sizeForItem返回(0,0)的核心原因是自定义TagView没有正确实现自动布局的内在尺寸计算,而且直接调用layoutSubviews没法在frame为zero的情况下算出正确大小。另外要实现CollectionView高度动态变化,还需要监听它的内容尺寸变化。下面一步步给你解决:
第一步:修复TagView的自动布局支持
首先修改你的TagView,让它能正确计算自身的内在尺寸(intrinsic content size),这是AutoLayout系统用来确定View大小的关键:
public class TagView: UIView { let textLabel: UILabel = { let label = UILabel() label.textColor = .black label.textAlignment = .center label.font = UIFont.systemFont(ofSize: 15) label.numberOfLines = 1 // 保持单行,按需可修改为多行 label.translatesAutoresizingMaskIntoConstraints = false return label }() override init(frame: CGRect) { super.init(frame: frame) setupView() setupLabel() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupView() { backgroundColor = #colorLiteral(red: 0.03529411765, green: 0.6862745098, blue: 0.003921568627, alpha: 1) layer.cornerRadius = 3 layer.masksToBounds = true translatesAutoresizingMaskIntoConstraints = false // 关闭自动转换约束,启用AutoLayout } private func setupLabel() { addSubview(textLabel) // 用AutoLayout设置label的内边距 NSLayoutConstraint.activate([ textLabel.topAnchor.constraint(equalTo: topAnchor, constant: 3), textLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 3), textLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -3), textLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -3) ]) } // 重写内在尺寸,让系统根据label内容+内边距计算大小 override public var intrinsicContentSize: CGSize { let labelSize = textLabel.intrinsicContentSize return CGSize(width: labelSize.width + 6, height: labelSize.height + 6) // 上下左右各3pt内边距,总和+6 } // 当label宽度变化时,通知系统重新计算内在尺寸 override public func layoutSubviews() { super.layoutSubviews() textLabel.preferredMaxLayoutWidth = textLabel.bounds.width invalidateIntrinsicContentSize() } }
第二步:正确实现sizeForItem方法
现在在sizeForItem里,用systemLayoutSizeFitting来计算TagView的正确尺寸,而不是直接调用layoutSubviews:
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let tagView = TagView() let tagText = tags[indexPath.row] tagView.textLabel.text = tagText // 使用systemLayoutSizeFitting获取View的最小适配尺寸 let fittingSize = tagView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) // 确保高度不低于字体行高+内边距,避免异常情况 let minHeight = tagView.textLabel.font.lineHeight + 6 return CGSize(width: fittingSize.width, height: max(fittingSize.height, minHeight)) }
第三步:实现UICollectionView动态高度
这里有两种常用方案,选一种适合你的:
方案1:KVO监听contentSize变化
通过监听CollectionView的contentSize属性,动态更新它的高度约束:
override func viewDidLoad() { super.viewDidLoad() let layout = UICollectionViewFlowLayout() layout.minimumInteritemSpacing = 8 // 设置item间横向间距 layout.minimumLineSpacing = 8 // 设置行间距 let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.dataSource = self collectionView.delegate = self collectionView.register(TagView.self, forCellWithReuseIdentifier: "TagCell") view.addSubview(collectionView) // 设置基础约束:左右、顶部固定 NSLayoutConstraint.activate([ collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20) ]) // 添加KVO监听contentSize collectionView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil) } // 处理KVO回调,更新高度约束 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard keyPath == "contentSize", let collectionView = object as? UICollectionView else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) return } // 先移除旧的高度约束(如果有的话) collectionView.constraints.forEach { constraint in if constraint.firstAttribute == .height { constraint.isActive = false } } // 添加新的高度约束 collectionView.heightAnchor.constraint(equalToConstant: collectionView.contentSize.height).isActive = true view.layoutIfNeeded() } // 记得在销毁时移除监听 deinit { collectionView.removeObserver(self, forKeyPath: "contentSize") }
方案2:自定义AutoSizingCollectionView子类
创建一个自动适配内容高度的CollectionView子类,重写intrinsicContentSize:
class AutoSizingCollectionView: UICollectionView { override var intrinsicContentSize: CGSize { return contentSize } override func layoutSubviews() { super.layoutSubviews() // 当内容尺寸变化时,通知系统更新内在尺寸 if !bounds.size.equalTo(intrinsicContentSize) { invalidateIntrinsicContentSize() } } }
使用这个子类时,只需要设置左右、顶部约束,高度会自动跟随内容变化:
override func viewDidLoad() { super.viewDidLoad() let layout = UICollectionViewFlowLayout() layout.minimumInteritemSpacing = 8 layout.minimumLineSpacing = 8 let collectionView = AutoSizingCollectionView(frame: .zero, collectionViewLayout: layout) collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.dataSource = self collectionView.delegate = self collectionView.register(TagView.self, forCellWithReuseIdentifier: "TagCell") view.addSubview(collectionView) NSLayoutConstraint.activate([ collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20), // 可选:设置最小高度,避免内容为空时高度为0 collectionView.heightAnchor.constraint(greaterThanOrEqualToConstant: 0) ]) }
这样修改后,你的CollectionView不仅能正确计算每个Tag单元格的尺寸,还能根据内容自动调整自身高度啦。
内容的提问来源于stack exchange,提问作者Cesare
相关产品推荐
相关产品推荐

