'Self sizing collection view enters a recursive loop in iOS 15

I have a self sizing collection view and when I call super.layoutSubviews my app crashes since the collection view enters a recursive update loop. This was working fine in iOS 14 and below. But observed it in iOS 15 onwards.

class DynamicCollectionView: UICollectionView {

override var contentSize: CGSize {
    didSet {
        invalidateIntrinsicContentSize()
    }
}

override func layoutSubviews() {
    super.layoutSubviews()
    if bounds.size != intrinsicContentSize {
        invalidateIntrinsicContentSize()
    }
}

override var intrinsicContentSize: CGSize {
    return contentSize
}

override func reloadData() {
    super.reloadData()
    invalidateIntrinsicContentSize()
    layoutIfNeeded()
}

}

Crash says:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView (<KPFlagship.SelfSizingCollectionView 0x7f896b260e00>) is stuck in its update/layout loop. This can happen for many reasons, including self-sizing views whose preferred attributes are not returning a consistent size. To debug this issue, check the Console app for logs in the "UICollectionViewRecursion" category.'



Solution 1:[1]

In our case (vertically flow layout collectioview, vertically self-sizing cells), problem was that I had some cells that didn't implement overriding preferredLayoutAttributesFitting:

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
    layoutAttributes.frame.size = contentView
        .systemLayoutSizeFitting(targetSize,
                                 withHorizontalFittingPriority: .required,
                                 verticalFittingPriority: .fittingSizeLevel)
    return layoutAttributes
}

here is flowlayout similar to what we had:

final class VerticalFlowLayout: UICollectionViewFlowLayout {
    
    override init() {
        super.init()
        scrollDirection = .vertical
        estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    }
        
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributesObjects = super.layoutAttributesForElements(in: rect)
        
        
        layoutAttributesObjects?.enumerated()
            .forEach { (index, layoutAttributes) in
                if layoutAttributes.representedElementCategory == .cell {
                    
                    guard let newFrame = layoutAttributesForItem(at: layoutAttributes.indexPath)?.frame else {
                        return
                    }
                    layoutAttributes.frame = newFrame
                }
            }
        
        return layoutAttributesObjects
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let collectionView = collectionView else {
            return nil
        }
        
        guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath) else {
            return nil
        }
        
        layoutAttributes.frame.origin.x = sectionInset.left
        layoutAttributes.frame.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right
        
        return layoutAttributes
    }
}

So, In ios versions before ios 15 collectionview and cells had no problem with it. After building the same code to ios 15+ started to stuck in its update/layout loop.

So, if you have a similar problem, try to figure out which type of custom layout you are using and try return according preferredLayoutAttributesFitting.

P.S. If you (person reading that) have any insights why it worked before and do not work after ios15, what actually has changed in ios15 that leads to such problem, please share with us, ty :)

Solution 2:[2]

Was facing the same crash on iOS15 when manually calculating cell size.

By disabling Estimated Size on collectionView in IB by setting it to None was fix for me.

Solution 3:[3]

I had similar problem in iOS 15. Fixed it by overriding UICollectionViewFlowLayout method:

override func shouldInvalidateLayout(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, 
         withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> Bool {
    return true
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Zaporozhchenko Oleksandr
Solution 2 semyrozum
Solution 3 Julia K.