Home » Swift » With UICollectionViewFlowLayout / UICollectionViewLayout subclass, CellForItemAtIndexPath is not called at the correct time

With UICollectionViewFlowLayout / UICollectionViewLayout subclass, CellForItemAtIndexPath is not called at the correct time

Posted by: admin January 9, 2018 Leave a comment

Questions:

This question is not a duplicate, although i see many on stavkOverflow similar. Here is the situation surrounding my code:

  1. CellForItemAtIndexPath – is called correctly if i have my collection view is outside of a UIStackView. Problem only happens when its inside of a stackView

  2. I have correctly subclassed my flowLayout from UICollectionViewFlowLayout.

  3. The CollectionView correctly returns the numberOfSections, and the NumberOfItems in each section. The number of items in each section is 42 (6 rows x 7 columns)

Here is what the problem looks like: You can see it will get to a point where the CellForItemAtIndexPath method is not called. There will be no cells there. Then suddenly, when you get to a certain point in scrolling, it will just be called immediately, causing the cells to appear.

enter image description here

Does any one know what the error could be? This error does not happen if the UICollectionView is not placed inside a UIStackView.

[edit]

Ok. I have narrowed to problem down to the UICollectionViewFlowLayout subclass. There is a short line of code in there to change the layout of the collectionView to horizontal. Meaning, cells on the view are normally rendered like this:

[1][4][7]
[2][5][8]
[3][6][9]

//But the code should render it like this
[1][2][3]
[4][5][6]
[7][8][9]

I have the following code to do this (taken from KDCalendar). This code works fine, but once its inside of a stackView, it looks what what is displayed above. Is the approach for changing the order for the cells different when inside a stackView? the following is the code causing the problem (only inside stackView)

override  public func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
    if let attrs = super.layoutAttributesForItemAtIndexPath(indexPath) {
        let attrscp = attrs.copy() as! UICollectionViewLayoutAttributes
        self.applyLayoutAttributes(attrscp)
        return attrscp
    }
    return nil
}

override public func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    return super.layoutAttributesForElementsInRect(rect)?.map {
        attrs in
        let attrscp = attrs.copy() as! UICollectionViewLayoutAttributes
        self.applyLayoutAttributes(attrscp)
        return attrscp
    }
}

func applyLayoutAttributes(attributes : UICollectionViewLayoutAttributes) {
    if attributes.representedElementKind != nil {return}
    if let collectionView = self.collectionView {
        let stride = (self.scrollDirection == .Horizontal) ? collectionView.frame.size.width : collectionView.frame.size.height
        let offset = CGFloat(attributes.indexPath.section) * stride
        var xCellOffset : CGFloat = CGFloat(attributes.indexPath.item % 7) * self.itemSize.width
        var yCellOffset : CGFloat = CGFloat(attributes.indexPath.item / 7) * self.itemSize.height
        if(self.scrollDirection == .Horizontal) {
            xCellOffset += offset;
        } else {
            yCellOffset += offset
        }
        attributes.frame = CGRectMake(xCellOffset, yCellOffset, self.itemSize.width, self.itemSize.height)
    }
}

So i guess basically my question is, what code is the fastest most efficient way to render the cells horizontally as shown above by subclassing the UICollectionViewFlowLayout? My collectionView will always have 7 columns. The rows can only be either 1, 2, 3 or 6.

[EDIT]

I have narrow the problem down to the following function:

func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    //Inside this function, I have put the following code to test it.
    print(super.layoutAttributesForElementsInRect(rect))
}

Here is the print out that i get

  • [0] : index path: ( {length = 2, path = 0 – 0}); frame
    = (0 0; 59.1429 72.8333);

    • 1 : index path: ( {length = 2, path = 0 – 1});
      frame = (0 73; 59.1429 72.8333);
    • [2] : index path: ( {length = 2, path = 0 – 2});
      frame = (0 145.667; 59.1429 72.8333);
    • [3] : index path: ( {length = 2, path = 0 – 3});
      frame = (0 218.667; 59.1429 72.8333);
    • [4] : index path: ( {length = 2, path = 0 – 4});
      frame = (0 291.333; 59.1429 72.8333);
    • [5] : index path: ( {length = 2, path = 0 – 5});
      frame = (0 364.333; 59.1429 72.8333);
    • [6] : index path: ( {length = 2, path = 0 – 6});
      frame = (59 0; 59.1429 72.8333);
    • [7] : index path: ( {length = 2, path = 0 – 7});
      frame = (59 73; 59.1429 72.8333);
    • [8] : index path: ( {length = 2, path = 0 – 8});
      frame = (59 145.667; 59.1429 72.8333);
    • [9] : index path: ( {length = 2, path = 0 – 9});
      frame = (59 218.667; 59.1429 72.8333);
    • [10] : index path: ( {length = 2, path = 0 – 10});
      frame = (59 291.333; 59.1429 72.8333);
    • [11] : index path: ( {length = 2, path = 0 – 11});
      frame = (59 364.333; 59.1429 72.8333);
    • [12] : index path: ( {length = 2, path = 0 – 12});
      frame = (118.333 0; 59.1429 72.8333);
    • [13] : index path: ( {length = 2, path = 0 – 13});
      frame = (118.333 73; 59.1429 72.8333);
    • [14] : index path: ( {length = 2, path = 0 – 14});
      frame = (118.333 145.667; 59.1429 72.8333);
    • [15] : index path: ( {length = 2, path = 0 – 15});
      frame = (118.333 218.667; 59.1429 72.8333);
    • [16] : index path: ( {length = 2, path = 0 – 16});
      frame = (118.333 291.333; 59.1429 72.8333);
    • [17] : index path: ( {length = 2, path = 0 – 17});
      frame = (118.333 364.333; 59.1429 72.8333);
    • [18] : index path: ( {length = 2, path = 0 – 18});
      frame = (177.333 0; 59.1429 72.8333);
    • [19] : index path: ( {length = 2, path = 0 – 19});
      frame = (177.333 73; 59.1429 72.8333);
    • [20] : index path: ( {length = 2, path = 0 – 20});
      frame = (177.333 145.667; 59.1429 72.8333);
    • [21] : index path: ( {length = 2, path = 0 – 21});
      frame = (177.333 218.667; 59.1429 72.8333);
    • [22] : index path: ( {length = 2, path = 0 – 22});
      frame = (177.333 291.333; 59.1429 72.8333);
    • [23] : index path: ( {length = 2, path = 0 – 23});
      frame = (177.333 364.333; 59.1429 72.8333);
    • [24] : index path: ( {length = 2, path = 0 – 24});
      frame = (236.667 0; 59.1429 72.8333);
    • [25] : index path: ( {length = 2, path = 0 – 25});
      frame = (236.667 73; 59.1429 72.8333);
    • [26] : index path: ( {length = 2, path = 0 – 26});
      frame = (236.667 145.667; 59.1429 72.8333);
    • [27] : index path: ( {length = 2, path = 0 – 27});
      frame = (236.667 218.667; 59.1429 72.8333);
    • [28] : index path: ( {length = 2, path = 0 – 28});
      frame = (236.667 291.333; 59.1429 72.8333);
    • [29] : index path: ( {length = 2, path = 0 – 29});
      frame = (236.667 364.333; 59.1429 72.8333);
    • [30] : index path: ( {length = 2, path = 0 – 30});
      frame = (295.667 0; 59.1429 72.8333);
    • [31] : index path: ( {length = 2, path = 0 – 31});
      frame = (295.667 73; 59.1429 72.8333);
    • [32] : index path: ( {length = 2, path = 0 – 32});
      frame = (295.667 145.667; 59.1429 72.8333);
    • [33] : index path: ( {length = 2, path = 0 – 33});
      frame = (295.667 218.667; 59.1429 72.8333);
    • [34] : index path: ( {length = 2, path = 0 – 34});
      frame = (295.667 291.333; 59.1429 72.8333);
    • [35] : index path: ( {length = 2, path = 0 – 35});
      frame = (295.667 364.333; 59.1429 72.8333);
    • [36] : index path: ( {length = 2, path = 0 – 36});
      frame = (355 0; 59.1429 72.8333);
    • [37] : index path: ( {length = 2, path = 0 – 37});
      frame = (355 73; 59.1429 72.8333);
    • [38] : index path: ( {length = 2, path = 0 – 38});
      frame = (355 145.667; 59.1429 72.8333);
    • [39] : index path: ( {length = 2, path = 0 – 39});
      frame = (355 218.667; 59.1429 72.8333);
    • [40] : index path: ( {length = 2, path = 0 – 40});
      frame = (355 291.333; 59.1429 72.8333);
    • [41] : index path: ( {length = 2, path = 0 – 41});
      frame = (355 364.333; 59.1429 72.8333);
    • [42] : index path: ( {length = 2, path = 1 – 0});
      frame = (414 0; 59.1429 72.8333);
    • [43] : index path: ( {length = 2, path = 1 – 1});
      frame = (414 73; 59.1429 72.8333);
    • [44] : index path: ( {length = 2, path = 1 – 2});
      frame = (414 145.667; 59.1429 72.8333);
    • [45] : index path: ( {length = 2, path = 1 – 3});
      frame = (414 218.667; 59.1429 72.8333);
    • [46] : index path: ( {length = 2, path = 1 – 4});
      frame = (414 291.333; 59.1429 72.8333);
    • [47] : index path: ( {length = 2, path = 1 – 5});
      frame = (414 364.333; 59.1429 72.8333);
    • [48] : index path: ( {length = 2, path = 1 – 6});
      frame = (473 0; 59.1429 72.8333);
    • [49] : index path: ( {length = 2, path = 1 – 7});
      frame = (473 73; 59.1429 72.8333);
    • [50] : index path: ( {length = 2, path = 1 – 8});
      frame = (473 145.667; 59.1429 72.8333);
    • [51] : index path: ( {length = 2, path = 1 – 9});
      frame = (473 218.667; 59.1429 72.8333);
    • [52] : index path: ( {length = 2, path = 1 – 10});
      frame = (473 291.333; 59.1429 72.8333);
    • [53] : index path: ( {length = 2, path = 1 – 11});
      frame = (473 364.333; 59.1429 72.8333);
    • [54] : index path: ( {length = 2, path = 1 – 12});
      frame = (532.333 0; 59.1429 72.8333);
    • [55] : index path: ( {length = 2, path = 1 – 13});
      frame = (532.333 73; 59.1429 72.8333);
    • [56] : index path: ( {length = 2, path = 1 – 14});
      frame = (532.333 145.667; 59.1429 72.8333);
    • [57] : index path: ( {length = 2, path = 1 – 15});
      frame = (532.333 218.667; 59.1429 72.8333);
    • [58] : index path: ( {length = 2, path = 1 – 16});
      frame = (532.333 291.333; 59.1429 72.8333);
    • [59] : index path: ( {length = 2, path = 1 – 17});
      frame = (532.333 364.333; 59.1429 72.8333);
    • [60] : index path: ( {length = 2, path = 1 – 18});
      frame = (591.333 0; 59.1429 72.8333);
    • [61] : index path: ( {length = 2, path = 1 – 19});
      frame = (591.333 73; 59.1429 72.8333);
    • [62] : index path: ( {length = 2, path = 1 – 20});
      frame = (591.333 145.667; 59.1429 72.8333);
    • [63] : index path: ( {length = 2, path = 1 – 21});
      frame = (591.333 218.667; 59.1429 72.8333);
    • [64] : index path: ( {length = 2, path = 1 – 22});
      frame = (591.333 291.333; 59.1429 72.8333);
    • [65] : index path: ( {length = 2, path = 1 – 23});
      frame = (591.333 364.333; 59.1429 72.8333);
    • [66] : index path: ( {length = 2, path = 1 – 24});
      frame = (650.667 0; 59.1429 72.8333);
    • [67] : index path: ( {length = 2, path = 1 – 25});
      frame = (650.667 73; 59.1429 72.8333);
    • [68] : index path: ( {length = 2, path = 1 – 26});
      frame = (650.667 145.667; 59.1429 72.8333);
    • [69] : index path: ( {length = 2, path = 1 – 27});
      frame = (650.667 218.667; 59.1429 72.8333);
    • [70] : index path: ( {length = 2, path = 1 – 28});
      frame = (650.667 291.333; 59.1429 72.8333);
    • [71] : index path: ( {length = 2, path = 1 – 29});
      frame = (650.667 364.333; 59.1429 72.8333);
    • [72] : index path: ( {length = 2, path = 1 – 30});
      frame = (709.667 0; 59.1429 72.8333);
    • [73] : index path: ( {length = 2, path = 1 – 31});
      frame = (709.667 73; 59.1429 72.8333);
    • [74] : index path: ( {length = 2, path = 1 – 32});
      frame = (709.667 145.667; 59.1429 72.8333);
    • [75] : index path: ( {length = 2, path = 1 – 33});
      frame = (709.667 218.667; 59.1429 72.8333);
    • [76] : index path: ( {length = 2, path = 1 – 34});
      frame = (709.667 291.333; 59.1429 72.8333);
    • [77] : index path: ( {length = 2, path = 1 – 35});
      frame = (709.667 364.333; 59.1429 72.8333);

I can see that the function is asking for the index paths: 0-0 to 0-41 for section 0 which is correct. BUT, it ONLY asks for index paths 1-0 to 1-35 for the other section. It is missing the other 6 indexPaths. It only presents the other 6 paths when the user scrolls the missing cells well into view. Is there a reason why it is not returning all index? Is this even the correct way to detect the cells in the rect?

Answers:

There is a fundamental flaw with your JTAppleCalendarFlowLayout.

The UICollectionView internally relies on layoutAttributesForElementsInRect() to know which cells to request from layoutAttributesForItemAtIndexPath. Since you are passing the rect to super without modification, it doesn’t correctly understand which cells should be visible.

To demonstrate the problem better, I made a second copy of the collection view that stays in sync, but disabled your applyLayoutAttributes modification on the bottom one. I also colored the cells which are appearing too late.

demo animation

The part you must understand is that the cells which appear “too late” in the top collectionView are not yet visible in the original/unmodified layout. The collection view adds them only when it thinks they’ll be needed soon. It doesn’t realize that you’re actually making them visible already.

So… how should you fix it?

Since your layout is so simple, I would recommend that you don’t bother with UICollectionViewFlowLayout, because it isn’t designed to handle “pages” this way. Just subclass UICollectionViewLayout, and implement layoutAttributesForElementsInRect and layoutAttributesForItemAtIndexPath yourself with custom calculations.

Or, if you are really set on using UICollectionViewFlowLayout, you will need to modify the rect you pass to super so that, under the original layout, the rect would encompass all cells which will be visible under your modified layout.

Questions:
Answers:

If anyone wants to see the implementation that i did with JTBandes’s excellent response, they can find the answer here on github. The layout was done for a iOS calendarView project.

The calendar view has 7 columns, a configurable number of rows 1, 2, 3, or 6.
The problem mentioned above occurred when the calendar was in horizontal mode.

The solution I adopted based on JTBandes’s answer was to subclass from UICollectionViewLayout instead of UICollectionViewFlowLayout. All of the code can be found in this file in the repository. You can also sample the entire project by checking out the Cocoapod on this link. Thanks all.