iOS 13 setting content inset on collection view with compositional layout causes jump

I'm working with compositional layout collection views and need to support back to iOS 13, but I'm running into a strange issue when I update the content inset of the collection view. I'm using estimated cell sizes because eventually, these cells will all house different content, so they need to be dynamic.

You can see the issue here below in the video. When I press "Update" I set the bottom content inset to be 100. You can see the content jumps. Then when I start to scroll to the top, as I'm nearly there, the content jumps again to a different position in the collection view.

This isn't an issue on iOS 15 or 16. On iOS 14, the content doesn't jump when I update the content insets, but it does jump to a different position in the collection view when I start scrolling after it's been updated.

This issue does

Here's my code below:

Main view controller

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)
    }
    
}

Cell

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 helpers

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)" }

}
iOS 13 setting content inset on collection view with compositional layout causes jump
 
 
Q