iOS 18中如何为UICollectionView实现动态高度的AutoLayout头部
解决iOS 18中UICollectionView头部自动适配高度的问题
核心问题分析
你遇到的问题根源有两点:
referenceSizeForHeaderInSection优先级更高:只要在代理方法中返回固定高度,FlowLayout会直接忽略头部视图自身的尺寸计算逻辑,导致preferredLayoutAttributesFitting完全无效。- iOS 18禁用手动出列视图:旧方案中手动出列模拟头部测量尺寸的方式,在iOS 18下会触发崩溃。
解决方案步骤
1. 开启FlowLayout自动尺寸计算
两种方式二选一,让FlowLayout知道需要依赖头部视图自身计算高度:
方式一:修改代理方法
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { // 固定宽度为CollectionView宽度,高度使用系统自动计算 return CGSize(width: collectionView.frame.width, height: UICollectionViewFlowLayout.automaticSize) }
方式二:直接配置FlowLayout(可删除代理方法)
在viewDidLoad中添加配置:
override func viewDidLoad() { super.viewDidLoad() // ... 原有注册代码 ... if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.headerReferenceSize = CGSize(width: collectionView.frame.width, height: UICollectionViewFlowLayout.automaticSize) } }
2. 正确实现头部视图的preferredLayoutAttributesFitting
启用自动尺寸后,FlowLayout会调用该方法获取实际高度,修改HeaderView中的实现:
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { // 先更新布局,确保文本和约束生效 layoutIfNeeded() // 固定宽度,让系统自动计算所需高度 let targetSize = CGSize(width: layoutAttributes.frame.width, height: UIView.layoutFittingCompressedSize.height) let fittedSize = systemLayoutSizeFitting( targetSize, withHorizontalFittingPriority: .required, // 宽度强制固定 verticalFittingPriority: .fittingSizeLevel // 高度自适应内容 ) // 返回更新后的布局属性 let newAttributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes newAttributes.frame.size = fittedSize return newAttributes }
3. 确认约束完整性
你的HeaderView约束已经完整(从顶部到底部有连续约束链),这是自动计算高度的必要条件,无需修改。
修改后的完整代码
SomeViewController
class SomeViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { let CellId = "CellId" let HeaderId = "HeaderId" @IBOutlet weak var collectionView: UICollectionView! override func viewDidLoad() { super.viewDidLoad() collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: CellId) collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HeaderId) // 配置FlowLayout自动计算头部高度 if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.headerReferenceSize = CGSize(width: collectionView.frame.width, height: UICollectionViewFlowLayout.automaticSize) } } func numberOfSections(in collectionView: UICollectionView) -> Int { return 2 } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return section == 0 ? 5 : 10 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellId, for: indexPath) cell.backgroundColor = indexPath.section == 0 ? .red : .blue return cell } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: 50, height: 50) } func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderId, for: indexPath) if let headerView = view as? HeaderView { if indexPath.section == 0 { headerView.configure(title: "Section 1", info: "This is section 1") } else { headerView.configure(title: "Section 2", info: "This is section 2 with a longer text. The height of the header is automatically adjusted to fit the text. And we make the text even longer to see how it works. And we make the text even longer to see how it works. And we make the text even longer to see how it works.") } } return view } }
HeaderView
class HeaderView: UICollectionReusableView { let titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .preferredFont(forTextStyle: .headline) label.textColor = .black label.numberOfLines = 0 label.backgroundColor = .white return label }() let infoLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .preferredFont(forTextStyle: .body) label.textColor = .black label.numberOfLines = 0 label.backgroundColor = .white return label }() override init(frame: CGRect) { super.init(frame: frame) setupView() } required init?(coder: NSCoder) { super.init(coder: coder) setupView() } private func setupView() { backgroundColor = .green addSubview(titleLabel) addSubview(infoLabel) NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20), titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), infoLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10), infoLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), infoLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), infoLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20) ]) } func configure(title: String, info: String) { titleLabel.text = title infoLabel.text = info } override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { layoutIfNeeded() let targetSize = CGSize(width: layoutAttributes.frame.width, height: UIView.layoutFittingCompressedSize.height) let fittedSize = systemLayoutSizeFitting( targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel ) let newAttributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes newAttributes.frame.size = fittedSize return newAttributes } }
原理说明
- 当FlowLayout设置为自动尺寸时,会完全依赖头部视图的
preferredLayoutAttributesFitting方法获取实际高度。 layoutIfNeeded()确保文本更新后约束已生效,避免计算错误尺寸。systemLayoutSizeFitting会根据视图的约束和内容,自动计算出适配的高度。
内容的提问来源于stack exchange,提问作者Andrei Herford
相关产品推荐
相关产品推荐

