Post

Replies

Boosts

Views

Activity

UICollectionViewLayout unexpected animations when cells contain AutoLayout views with custom height
The code for the issue is attached below. Hello, I am trying to implement a custom UICollectionViewLayout that does the following: Everything works great for the most part, however I have encountered some unexpected animations when applying a new snapshot: As you can see, any cell that contains a custom view with a height set with AutoLayout is scaled vertically before animating to it's intended height. Here is a simple Xcode project that demonstrates the issue. Tap on the plus sign in the top right corner and watch the cells. Example project: https://we.tl/t-9Y25NHzxiI Custom UICollectionViewLayout code: final class CustomLayout: UICollectionViewLayout { struct PMCardContainerLayoutCell: Equatable { var column: Int var row: Int } // Configurable properties public var numberOfColumns: Int = 6 public var cellHeight: Double = 100 public var cellSpacing: Double = 20 public var rowSpacing: Double = 20 public var sectionInsets: NSDirectionalEdgeInsets = .zero public var layoutAttributes: [IndexPath: UICollectionViewLayoutAttributes] = [:] override func prepare() { super.prepare() guard let collectionView else { return } var updatedLayoutAttributes: [IndexPath: 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 occupiedCells: [PMCardContainerLayoutCell] = [] 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) let itemSpanColumn = 1 let itemHeight = layoutAttributes[itemIndexPath]?.bounds.height ?? 140 let itemSpanRow = Int(ceil(itemHeight / (cellHeight + rowSpacing))) let itemWidth = columnWidth * Double(itemSpanColumn) + cellSpacing * (Double(itemSpanColumn) - 1) while true { var itemDoesFit: Bool = true if currentColumn + itemSpanColumn > numberOfColumns { currentColumn = 0 currentRow += 1 } for cell in 0..<itemSpanColumn { if occupiedCells.contains(.init(column: currentColumn + cell, row: currentRow)) { itemDoesFit = false } } if itemDoesFit { break } currentColumn += itemSpanColumn } if itemSpanRow > 1 { for row in 1..<itemSpanRow { for column in 0..<itemSpanColumn { occupiedCells.append(.init(column: currentColumn + column, row: currentRow + row)) } } } let originX = sectionInsets.leading + columnWidth * Double(currentColumn) + cellSpacing * Double(currentColumn) let originY = + cellHeight * Double(currentRow) + rowSpacing * Double(currentRow) itemAttributes.frame = CGRect( x: originX, y: originY, width: itemWidth, height: itemHeight ) itemAttributes.zIndex = itemIndexPath.section * 10 + itemIndexPath.item updatedLayoutAttributes[itemIndexPath] = itemAttributes currentColumn += itemSpanColumn } } 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? { return layoutAttributes[indexPath] } 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(forBoundsChange newBounds: CGRect) -> Bool { return true } override func shouldInvalidateLayout(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> Bool { return originalAttributes.frame.height != preferredAttributes.frame.height } override func invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext { layoutAttributes[preferredAttributes.indexPath]?.frame.size = preferredAttributes.frame.size let context = super.invalidationContext(forPreferredLayoutAttributes: preferredAttributes, withOriginalAttributes: originalAttributes) return context } public override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { super.invalidateLayout(with: context) if context.invalidateEverything || context.invalidateDataSourceCounts { layoutAttributes.removeAll() } } } Anyone have any idea what I am doing wrong? Thank you!
1
0
282
Aug ’24