UICollectionView not obeying zIndex of UICollectionViewLayoutAttributes

The effect I'm trying to achieve is a kind of sticky header cell. It's important to me that the sticky cell floats over the top of the others. Something a bit like this:

    ┌──────────┐
    │          │
    │  Cell 0  │
    │          ├┐
    └┬─────────┘│
     │  Cell 4  │
     │          │
     └──────────┘
     ┌──────────┐
     │          │
     │  Cell 5  │
     │          │
     └──────────┘
     ┌──────────┐
     │          │
     │  Cell 6  │
     │          │
     └──────────┘


Cell 4, 5 and 6 would normally viewable and I'm constructing the attributes for Cell 0 in my UICollectionViewFlowLayout subclass during layoutAttributesForElementsInRect:. All I do is call the super implementation, determine which cell I need to add in and then construct the UICollectionViewLayoutAttributes(forCellWithIndexPath:). I then set the zIndex for it to 1 (default is 0).


The problem I'm getting is that the UICollectionView seems to always ignore the zIndex

    ┌──────────┐
    │          │
    │  Cell 0  │
    │┌─────────┴┐
    └┤          │
     │  Cell 4  │
     │          │
     └──────────┘
     ┌──────────┐
     │          │
     │  Cell 5  │
     │          │
     └──────────┘
     ┌──────────┐
     │          │
     │  Cell 6  │
     │          │
     └──────────┘


Now I believe it's possible to visually sort this out using a 3d transform, but that doesn't work for me as I don't want any taps going to the cell which is over the top. So in this example I don't want Cell 4 receiving taps intended for Cell 0.


Does anyone have any ideas? This is on iOS 8.4.

Replies

Are you removing the Cell 0 from layout attributes if it exists before adding the new one?

Hi,


I don't have any problems using zIndex, but I'm using my own custom layout, no flowLayout subclass. Maybe you can show some code where you set zIndex.


Dirk

It's only inserted when it isn't part of the layout attributes array already.

Any luck with this? I'm having a very similar problem, albeit on iOS 9 only.

What if you try the following UICollectionViewFlowLayout subclass? Does it work?


import UIKit

class FloatingHeaderLayout: UICollectionViewFlowLayout {
    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
        return true
    }
  
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard var attributesArray = super.layoutAttributesForElementsInRect(rect) else { return nil }
      
        let contentOffset = collectionView?.contentOffset ?? CGPointZero
      
        let missingSectionsAttributesArray = missingSectionsAttributes(attributesArray)
        attributesArray += missingSectionsAttributesArray
      
        attributesArray.forEach { attributes in
            if attributes.representedElementCategory == .SupplementaryView && attributes.representedElementKind == UICollectionElementKindSectionHeader {
                attributes.frame.origin.y = contentOffset.y
                attributes.zIndex = 1
            }
        }
      
        return attributesArray
    }
  
    private func missingSectionsAttributes(attributesArray: [UICollectionViewLayoutAttributes]) -> [UICollectionViewLayoutAttributes] {
        var array: [UICollectionViewLayoutAttributes] = []
        var missingSections: Set<Int> = []
      
        attributesArray.forEach { attributes in
            if attributes.representedElementCategory == .Cell {
                missingSections.insert(attributes.indexPath.section)
            }
        }
      
        attributesArray.forEach { attributes in
            if attributes.representedElementCategory == .SupplementaryView && attributes.representedElementKind == UICollectionElementKindSectionHeader {
                missingSections.remove(attributes.indexPath.section)
            }
        }
      
        missingSections.forEach { missingSection in
            let indexPath = NSIndexPath(forItem: 0, inSection: missingSection)
            let attributes = layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)
          
            if let attributes = attributes {
                array.append(attributes)
            }
        }
      
        return array
    }
}


E: Now I realized you want floating cells, not a header. But it should work similarly.

Version with floating first cell:


import UIKit

class FloatingLayout: UICollectionViewFlowLayout {
    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
        return true
    }
   
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let layoutAttributes = super.layoutAttributesForElementsInRect(rect) else { return nil }
        var attributesArray = layoutAttributes.map { $0.copy() } as! [UICollectionViewLayoutAttributes]
       
        let contentOffset = collectionView?.contentOffset ?? CGPointZero
       
        let missingRowAttributesArray = missingRowsAttributes(attributesArray)
        attributesArray += missingRowAttributesArray
       
        attributesArray.forEach { attributes in
            if attributes.representedElementCategory == .Cell {
                if attributes.indexPath.item  == 0 {
                    attributes.frame.origin.y = contentOffset.y
                    attributes.zIndex = 1
                } else {
                   
                }
            }
        }
       
        return attributesArray
    }
   
    private func missingRowsAttributes(attributesArray: [UICollectionViewLayoutAttributes]) -> [UICollectionViewLayoutAttributes] {
        var array: [UICollectionViewLayoutAttributes] = []
        var missingRows: Set<Int> = [0]
       
        attributesArray.forEach { attributes in
            if attributes.representedElementCategory == .Cell {
                missingRows.remove(attributes.indexPath.item)
            }
        }
       
        missingRows.forEach { missingRow in
            let indexPath = NSIndexPath(forItem: missingRow, inSection: 0)
            let attributes = layoutAttributesForItemAtIndexPath(indexPath)
           
            if let attributes = attributes {
                array.append(attributes.copy() as! UICollectionViewLayoutAttributes)
            }
        }
       
        return array
    }
}

I was invalidating the layout by calling performBatchUpdates. Turns out that does not work correctly anymore on iOS 9, the items' zIndex does not get updated like it did before.

Could you clarify the issue of calling performCatchUpdates? And how you resolved the issue? I am targetting iOS 9 too and worried that my yet to be written custom layout might have similar issues. Thanks.

When the user selected an item, I used to call

[collectionView performBatchUpdates:nil completion:nil];

This worked fine until iOS 9, where it stopped properly updating the items' zIndex. So now I'm calling

[collectionView.collectionViewLayout invalidateLayout];

inside an animation block. The result isn't 100% the same as before, but will do for now.

cell.layer.zPosition = CGFloat(array.count - indexPath.row)