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 } }
Topic: UI Frameworks SubTopic: UIKit Tags:
Aug ’24
Reply to Constant HTTP 502 Errors in Sandbox
Same here
Replies
Boosts
Views
Activity
Jun ’24
Reply to Sandbox users: "Something went wrong. Try again later."
It seems this has been an issue for over a week now. I have already tried everything, from a different browser to a different network, still nothing.
Replies
Boosts
Views
Activity
Jun ’24
Reply to Sandbox users: "Something went wrong. Try again later."
Seems to be working now.
Replies
Boosts
Views
Activity
Jun ’24
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 } }
Topic: UI Frameworks SubTopic: UIKit Tags:
Replies
Boosts
Views
Activity
Aug ’24