struct ColumnarCollectionViewLayoutSectionInvalidationResults { let invalidatedHeaderIndexPaths: [IndexPath] let invalidatedItemIndexPaths: [IndexPath] let invalidatedFooterIndexPaths: [IndexPath] static let empty: ColumnarCollectionViewLayoutSectionInvalidationResults = ColumnarCollectionViewLayoutSectionInvalidationResults(invalidatedHeaderIndexPaths: [], invalidatedItemIndexPaths: [], invalidatedFooterIndexPaths: []) } public class ColumnarCollectionViewLayoutInfo { var sections: [ColumnarCollectionViewLayoutSection] = [] var contentSize: CGSize = .zero func layout(with metrics: ColumnarCollectionViewLayoutMetrics, delegate: ColumnarCollectionViewLayoutDelegate, collectionView: UICollectionView, invalidationContext context: ColumnarCollectionViewLayoutInvalidationContext?) { guard let dataSource = collectionView.dataSource else { return } guard let countOfSections = dataSource.numberOfSections?(in: collectionView), countOfSections > 0 else { return } sections.reserveCapacity(countOfSections) let x = metrics.layoutMargins.left var y = metrics.layoutMargins.top let width = metrics.boundsSize.width - metrics.layoutMargins.left - metrics.layoutMargins.right for sectionIndex in 0.. UICollectionViewLayoutAttributes? { guard sections.indices.contains(indexPath.section) else { return nil } let section = sections[indexPath.section] guard section.items.indices.contains(indexPath.item) else { return nil } return section.items[indexPath.item] } public func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { guard sections.indices.contains(indexPath.section) else { return nil } let section = sections[indexPath.section] switch elementKind { case UICollectionView.elementKindSectionHeader: guard section.headers.indices.contains(indexPath.item) else { return nil } return section.headers[indexPath.item] case UICollectionView.elementKindSectionFooter: guard section.footers.indices.contains(indexPath.item) else { return nil } return section.footers[indexPath.item] default: return nil } } } class ColumnarCollectionViewLayoutInvalidationContext: UICollectionViewLayoutInvalidationContext { var originalLayoutAttributes: UICollectionViewLayoutAttributes? var preferredLayoutAttributes: UICollectionViewLayoutAttributes? var boundsDidChange: Bool = false } public class ColumnarCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes { public var precalculated: Bool = false public var layoutMargins: UIEdgeInsets = .zero override public func copy(with zone: NSZone? = nil) -> Any { let copy = super.copy(with: zone) guard let la = copy as? ColumnarCollectionViewLayoutAttributes else { return copy } la.precalculated = precalculated la.layoutMargins = layoutMargins return la } } public struct ColumnarCollectionViewLayoutHeightEstimate { public var precalculated: Bool public var height: CGFloat public init(precalculated: Bool, height: CGFloat) { self.precalculated = precalculated self.height = height } }