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