iOS13下Compositional Layout的CollectionView设置content inset出现跳转问题
问题描述
使用Compositional Layout的UICollectionView,需兼容iOS 13。因单元格内容动态可变,采用了预估单元格尺寸,但更新collectionView的content inset时出现异常:
- 点击“Update”按钮将底部content inset设为100时,内容发生跳转;
- 滚动至接近顶部时,内容再次跳转至其他位置。
该问题仅在iOS 13/14存在:iOS 15、16无此问题;iOS 14中更新inset时无跳转,但滚动时仍会出现位置跳转。
主视图控制器
enum Section { case main } class ViewController: CollectionViewController { private lazy var collectionViewDataSource: UICollectionViewDiffableDataSource<Section, Int> = { let dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) { [weak self] collectionView, indexPath, value in let cell = self?.cell(with: CollectionViewCell.self, for: indexPath, in: collectionView) cell?.configure(with: value) return cell } return dataSource }() private lazy var collectionView: UICollectionView = { let collectionViewLayout = configureChatCollectionViewLayout() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) collectionView.translatesAutoresizingMaskIntoConstraints = false registerCell(CollectionViewCell.self, in: collectionView) return collectionView }() private lazy var updateButton: UIButton = { let button = UIButton() button.setTitle("Update", for: .normal) button.setTitleColor(.blue, for: .normal) button.addTarget(self, action: #selector(updateTapped), for: .touchUpInside) button.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ button.heightAnchor.constraint(equalToConstant: 44) ]) return button }() override func viewDidLoad() { super.viewDidLoad() setupView() setDataItems(Array(0..<100)) } // MARK: - Interface methods func setDataItems(_ values: [Int]) { var snapshot = NSDiffableDataSourceSnapshot<Section, Int>() snapshot.appendSections([.main]) snapshot.appendItems(values) collectionViewDataSource.apply(snapshot, animatingDifferences: true) } // MARK: - Private methods func setupView() { view.backgroundColor = .white view.addSubview(updateButton) view.addSubview(collectionView) NSLayoutConstraint.activate([ updateButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), updateButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), updateButton.trailingAnchor.constraint(equalTo: view.trailingAnchor), collectionView.topAnchor.constraint(equalTo: updateButton.bottomAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) } func configureChatCollectionViewLayout() -> UICollectionViewLayout { let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(1000)) let item = NSCollectionLayoutItem(layoutSize: size) let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item]) let section = NSCollectionLayoutSection(group: group) section.interGroupSpacing = 16 return UICollectionViewCompositionalLayout(section: section) } @objc func updateTapped() { collectionView.contentInset = .init(top: 0, left: 0, bottom: 100, right: 0) } }
单元格
class CollectionViewCell: UICollectionViewCell { private lazy var textLabel: UILabel = { let label = UILabel() label.font = .preferredFont(forTextStyle: .title2) label.textColor = .black label.translatesAutoresizingMaskIntoConstraints = false return label }() override func prepareForReuse() { super.prepareForReuse() textLabel.text = nil } func configure(with value: Int) { contentView.backgroundColor = .green textLabel.text = "\(value)" contentView.addSubview(textLabel) NSLayoutConstraint.activate([ textLabel.topAnchor.constraint(equalTo: contentView.topAnchor), textLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), textLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), textLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) ]) } }
UICollectionView 辅助类
class CollectionViewController: UIViewController { func registerCell<T: UICollectionViewCell>(_ type: T.Type, in collectionView: UICollectionView) { collectionView.register(type, forCellWithReuseIdentifier: type.computedReuseIdentifier) } func cell<T: UICollectionViewCell>(with type: T.Type, for indexPath: IndexPath, in collectionView: UICollectionView) -> T { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: type.computedReuseIdentifier, for: indexPath) as? T else { assertionFailure("UICollectionViewCell cannot be cast to the intended type") return T() } return cell } } extension UICollectionViewCell { class var computedReuseIdentifier : String { "\(self)" } }
解决方案
1. 修复单元格约束重复添加问题
当前CollectionViewCell的configure方法每次都会重复添加子视图和约束,导致约束冲突,干扰预估尺寸计算。修改为仅在初始化时设置布局:
class CollectionViewCell: UICollectionViewCell { private lazy var textLabel: UILabel = { let label = UILabel() label.font = .preferredFont(forTextStyle: .title2) label.textColor = .black label.translatesAutoresizingMaskIntoConstraints = false return label }() override init(frame: CGRect) { super.init(frame: frame) setupSubviews() } required init?(coder: NSCoder) { super.init(coder: coder) setupSubviews() } override func prepareForReuse() { super.prepareForReuse() textLabel.text = nil } private func setupSubviews() { contentView.backgroundColor = .green contentView.addSubview(textLabel) NSLayoutConstraint.activate([ textLabel.topAnchor.constraint(equalTo: contentView.topAnchor), textLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), textLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), textLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) ]) } func configure(with value: Int) { textLabel.text = "\(value)" } }
2. 优化Content Inset更新逻辑
iOS 13/14中直接修改contentInset易引发布局偏移,建议结合批量更新和偏移量恢复:
@objc func updateTapped() { let currentOffset = collectionView.contentOffset collectionView.performBatchUpdates({ collectionView.contentInset = .init(top: 0, left: 0, bottom: 100, right: 0) }) { [weak self] _ in self?.collectionView.setContentOffset(currentOffset, animated: false) } }
3. 调整预估尺寸至合理值
当前预估高度1000与实际单元格高度差距过大,导致布局计算偏差,设置接近实际的预估高度:
func configureChatCollectionViewLayout() -> UICollectionViewLayout { let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) let item = NSCollectionLayoutItem(layoutSize: size) let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item]) let section = NSCollectionLayoutSection(group: group) section.interGroupSpacing = 16 return UICollectionViewCompositionalLayout(section: section) }
4. 优化滚动位置保持配置
在iOS 13中,设置contentInsetAdjustmentBehavior并确保滚动位置稳定:
private lazy var collectionView: UICollectionView = { let collectionViewLayout = configureChatCollectionViewLayout() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.contentInsetAdjustmentBehavior = .scrollableAxes registerCell(CollectionViewCell.self, in: collectionView) return collectionView }()
内容的提问来源于stack exchange,提问作者Mr.P
相关产品推荐
相关产品推荐

