Apparent lag when scrolling two-way scrolling UICollectionView with a large number of cells (250,000 or more)

I will subclass UICollectionViewFlowLayout to get two-way scrolling in a UICollectionView . Scrolling works fine for fewer rows and sections (100-200 rows and sections), but when scrolling, I can see that I increase the number of rows and sections by 500 ie 250,000 or more cells in the UICollectionView . I traced the source of the delay for the loop in layoutAttributesForElementsInRect . I use Dictionary to store UICollectionViewLayoutAttributes for each cell, to avoid recalculating and scrolling through it, to return cell attributes from layoutAttributesForElementsInRect

 import UIKit class LuckGameCollectionViewLayout: UICollectionViewFlowLayout { // Used for calculating each cells CGRect on screen. // CGRect will define the Origin and Size of the cell. let CELL_HEIGHT = 70.0 let CELL_WIDTH = 70.0 // Dictionary to hold the UICollectionViewLayoutAttributes for // each cell. The layout attribtues will define the cell size // and position (x, y, and z index). I have found this process // to be one of the heavier parts of the layout. I recommend // holding onto this data after it has been calculated in either // a dictionary or data store of some kind for a smooth performance. var cellAttrsDictionary = Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>() // Defines the size of the area the user can move around in // within the collection view. var contentSize = CGSize.zero override func collectionViewContentSize() -> CGSize { return self.contentSize } override func prepareLayout() { // Cycle through each section of the data source. if collectionView?.numberOfSections() > 0 { for section in 0...collectionView!.numberOfSections()-1 { // Cycle through each item in the section. if collectionView?.numberOfItemsInSection(section) > 0 { for item in 0...collectionView!.numberOfItemsInSection(section)-1 { // Build the UICollectionVieLayoutAttributes for the cell. let cellIndex = NSIndexPath(forItem: item, inSection: section) let xPos = Double(item) * CELL_WIDTH let yPos = Double(section) * CELL_HEIGHT let cellAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: cellIndex) cellAttributes.frame = CGRect(x: xPos, y: yPos, width: CELL_WIDTH, height: CELL_HEIGHT) // Save the attributes. cellAttrsDictionary[cellIndex] = cellAttributes } } } } // Update content size. let contentWidth = Double(collectionView!.numberOfItemsInSection(0)) * CELL_WIDTH let contentHeight = Double(collectionView!.numberOfSections()) * CELL_HEIGHT self.contentSize = CGSize(width: contentWidth, height: contentHeight) } override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { // Create an array to hold all elements found in our current view. var attributesInRect = [UICollectionViewLayoutAttributes]() // Check each element to see if it should be returned. for (_,cellAttributes) in cellAttrsDictionary { if CGRectIntersectsRect(rect, cellAttributes.frame) { attributesInRect.append(cellAttributes) } } // Return list of elements. return attributesInRect } override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { return cellAttrsDictionary[indexPath]! } override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { return false } } 

Edit: The following are the changes I used in the layoutAttributesForElementsInRect method.

 override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { // Create an array to hold all elements found in our current view. var attributesInRect = [UICollectionViewLayoutAttributes]() let xOffSet = self.collectionView?.contentOffset.x let yOffSet = self.collectionView?.contentOffset.y let totalColumnCount = self.collectionView?.numberOfSections() let totalRowCount = self.collectionView?.numberOfItemsInSection(0) let startRow = Int(Double(xOffSet!)/CELL_WIDTH) - 10 //include 10 rows towards left let endRow = Int(Double(xOffSet!)/CELL_WIDTH + Double(Utils.getScreenWidth())/CELL_WIDTH) + 10 //include 10 rows towards right let startCol = Int(Double(yOffSet!)/CELL_HEIGHT) - 10 //include 10 rows towards top let endCol = Int(Double(yOffSet!)/CELL_HEIGHT + Double(Utils.getScreenHeight())/CELL_HEIGHT) + 10 //include 10 rows towards bottom for(var i = startRow ; i <= endRow; i = i + 1){ for (var j = startCol ; j <= endCol; j = j + 1){ if (i < 0 || i > (totalRowCount! - 1) || j < 0 || j > (totalColumnCount! - 1)){ continue } let indexPath: NSIndexPath = NSIndexPath(forRow: i, inSection: j) attributesInRect.append(cellAttrsDictionary[indexPath]!) } } // Return list of elements. return attributesInRect } 

I calculated the offset of the View collection and used it to calculate the cells that will be visible on the screen (using the height / width of each cell). I had to add additional cells on each side so that when scrolling the user there were no cells. I tested this and performance is fine.

+8
ios swift uicollectionview uicollectionviewlayout
source share
3 answers

Using layoutAttributesForElementsInRect(rect: CGRect) with your known cell size, you do not need to cache your attributes and just calculate them for a given rect , as collectionView requests them. You still need to check the boundary cases 0 and the maximum number of sections / rows to avoid calculating unnecessary or invalid attributes, but this is easy to do in where clauses around loops. Here is a working example that I tested with 1000 sections x 1000 lines, and it works fine without delay on the device:

Edit: I added largeRect so that the attributes can be pre-calculated before the scroll is there. From your editing, it looks like you are still caching attributes that I think are not needed for performance. It will also lead to significantly larger memory with more scrolling. Also, is there a reason why you don't want to use the provided CGRect from the callback, rather than manually calculating it from contentOffset?

 class LuckGameCollectionViewLayout: UICollectionViewFlowLayout { let CELL_HEIGHT = 50.0 let CELL_WIDTH = 50.0 override func collectionViewContentSize() -> CGSize { let contentWidth = Double(collectionView!.numberOfItemsInSection(0)) * CELL_WIDTH let contentHeight = Double(collectionView!.numberOfSections()) * CELL_HEIGHT return CGSize(width: contentWidth, height: contentHeight) } override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let biggerRect = rect.insetBy(dx: -2048, dy: -2048) let startIndexY = Int(Double(biggerRect.origin.y) / CELL_HEIGHT) let startIndexX = Int(Double(biggerRect.origin.x) / CELL_WIDTH) let numberOfVisibleCellsInRectY = Int(Double(biggerRect.height) / CELL_HEIGHT) + startIndexY let numberOfVisibleCellsInRectX = Int(Double(biggerRect.width) / CELL_WIDTH) + startIndexX var attributes: [UICollectionViewLayoutAttributes] = [] for section in startIndexY..<numberOfVisibleCellsInRectY where section >= 0 && section < self.collectionView!.numberOfSections() { for item in startIndexX..<numberOfVisibleCellsInRectX where item >= 0 && item < self.collectionView!.numberOfItemsInSection(section) { let cellIndex = NSIndexPath(forItem: item, inSection: section) if let attrs = self.layoutAttributesForItemAtIndexPath(cellIndex) { attributes.append(attrs) } } } return attributes } override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { let xPos = Double(indexPath.row) * CELL_WIDTH let yPos = Double(indexPath.section) * CELL_HEIGHT let cellAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath) cellAttributes.frame = CGRect(x: xPos, y: yPos, width: CELL_WIDTH, height: CELL_HEIGHT) return cellAttributes } } 
+4
source share

You need to create a data structure for your cells, which is sorted by size (s) in order to come up with some kind of algorithm, using these measurements to narrow the search range.

Take the case of a table with cells 100 pixels high over the full width of the view and 250_000 elements, asking the cells to intersect with {0, top, 320, bottom}. Then your data structure will be an array ordered by top coordinate, and the accompanying algorithm will look like

 let start: Int = top / 100 let end: Int = bottom / 100 + 1 return (start...end).map { cellAttributes[$0] } 

Add as much complexity as your actual layout requires.

+1
source share

cellAttrsDictionary it is impractical to store a UICollectionViewLayoutAttributes . It is indexed by NSIndexPath , but in layoutAttributesForElementsInRect you are looking for an area. If you need cellAttrsDictionary for something else, you can leave it, but save each cellAttribute element additionally in a different data structure, which is searched faster.

For example, a nested array:

 var allCellAttributes = [[UICollectionViewLayoutAttributes]]() 

An array of the first level describes the area, for example, in pieces of 1000 pixels high and the width of the screen. Thus, all cellAttributes that intersect with the rectangle {0, 0, screen width, 1000} are included in:

 allCellAttributes[0].append(cellAttributes) 

All cellAttributes attributes that intersect with the rectangle {0, 1000, screen width, 2000} are included in:

 allCellAttributes[1].append(cellAttributes) 

And so on...

Then in layoutAttributesForElementsInRect you can search in this data structure by jumping directly into the array depending on the given CGRect. If the line is, for example, direct. {0, 5500, 100, 6700}, you just need to search in this range:

 allCellAttributes[5] allCellAttributes[6] 

This should give you the basic idea, I hope you understand what I mean.

+1
source share

All Articles