Post

Replies

Boosts

Views

Activity

Reply to UICollectionViewLayout unexpected animations when cells contain AutoLayout views with custom height
It seems I have solved the issue by using item IDs instead of IndexPaths to cache layout attributes. Here is the final UICollectionViewLayout code (rough draft, not cleaned up): final class CustomLayout: UICollectionViewLayout { // Configurable properties public var numberOfColumns: Int = 6 public var cellHeight: Double = 200 public var cellSpacing: Double = 20 public var rowSpacing: Double = 20 public var sectionInsets: NSDirectionalEdgeInsets = .zero public var layoutAttributes: [String: UICollectionViewLayoutAttributes] = [:] private var collectionViewDataSource: UICollectionViewDiffableDataSource<String, String>? { return collectionView?.dataSource as? UICollectionViewDiffableDataSource<String, String> } override func prepare() { super.prepare() guard let collectionView, let collectionViewDataSource else { return } var updatedLayoutAttributes: [String: UICollectionViewLayoutAttributes] = [:] let columnWidth: Double = (collectionView.bounds.width - cellSpacing * Double(numberOfColumns - 1) - sectionInsets.leading - sectionInsets.trailing) / Double(numberOfColumns) let numberOfSections: Int = collectionView.numberOfSections for section in 0..<numberOfSections { var currentColumn: Int = 0 var currentRow: Int = 0 let numberOfItems: Int = collectionView.numberOfItems(inSection: section) for item in 0..<numberOfItems { let itemIndexPath = IndexPath(item: item, section: section) let itemAttributes = UICollectionViewLayoutAttributes(forCellWith: itemIndexPath) guard let itemID = collectionViewDataSource.itemIdentifier(for: itemIndexPath) else { return } let itemHeight = layoutAttributes[itemID]?.bounds.height ?? 140 let itemWidth = columnWidth if currentColumn + 1 > numberOfColumns { currentColumn = 0 currentRow += 1 } let originX = sectionInsets.leading + columnWidth * Double(currentColumn) + cellSpacing * Double(currentColumn) let originY = sectionInsets.top + cellHeight * Double(currentRow) + rowSpacing * Double(currentRow) if let existingAttributes = layoutAttributes[itemID] { print("Using existing attributes for: \(existingAttributes.indexPath)") itemAttributes.frame = CGRect( x: originX, y: originY, width: existingAttributes.frame.width, height: existingAttributes.frame.height ) } else { itemAttributes.frame = CGRect( x: originX, y: originY, width: itemWidth, height: itemHeight ) } itemAttributes.zIndex = itemIndexPath.item updatedLayoutAttributes[itemID] = itemAttributes currentColumn += 1 } } layoutAttributes = updatedLayoutAttributes } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var allAttributes: [UICollectionViewLayoutAttributes] = [] for (_, attributes) in layoutAttributes { if (rect.intersects(attributes.frame)) { allAttributes.append(attributes) } } return allAttributes } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { guard let collectionViewDataSource else { return nil } guard let itemID = collectionViewDataSource.itemIdentifier(for: indexPath) else { return nil } return layoutAttributes[itemID] } override var collectionViewContentSize: CGSize { guard let collectionView else { return .zero } let contentHeight: CGFloat = layoutAttributes.map({ $0.value.frame.maxY }).max() ?? 0 return CGSize(width: collectionView.bounds.width, height: contentHeight) } override func shouldInvalidateLayout(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> Bool { return originalAttributes.frame.height.rounded() != preferredAttributes.frame.height.rounded() } override func invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext { let context = super.invalidationContext(forPreferredLayoutAttributes: preferredAttributes, withOriginalAttributes: originalAttributes) guard let collectionViewDataSource else { return context } guard let itemID = collectionViewDataSource.itemIdentifier(for: preferredAttributes.indexPath) else { return context } layoutAttributes[itemID]?.frame.size = preferredAttributes.frame.size context.invalidateItems(at: [preferredAttributes.indexPath]) return context } }
Aug ’24