You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 23:35:01