'In UICollectionView not obeying zIndex of UICollectionViewLayoutAttributes

I'm trying to make the top cell of the collection be behind all the other cells by the zIndex factor

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

In custom layout in method prepare I add Zindex as follows

override func prepare() {
  super.prepare()
  /// Some code
  
  let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
  attributes.zIndex = zIndex
  
  /// Some code
}

Next, I add the activation of this zIndex in UICollectionViewCell

override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
    super.apply(layoutAttributes)
    layer.zPosition = CGFloat(layoutAttributes.zIndex)
}

When reusing a cell. The topmost cell, which should be in the background, becomes in the foreground

Tell me who faced this problem. What am I doing wrong 🙏



Solution 1:[1]

I was able to achieve what you wanted with a custom flow layout.

Here is the result with a regular flow layout

UICollectionViewFlowLayout custom swift iOS

I create a custom flow layout class so that the cells could be in 1 column and the second cell overlaps the first

fileprivate class OverlapFlowLayout: UICollectionViewFlowLayout {
    
    // Y Offset for the cells after the first
    let initialYOffset: CGFloat = 30
    
    // X Offset the cells after the first
    let xOffset: CGFloat = 20
    
    // Vertical spacing between cells
    let cellSpacing: CGFloat = 10
    
    // Store the updated content height
    var contentHeight: CGFloat = .zero
    
    // Cache layout attributes for the cells
    private var cellLayoutCache: [IndexPath: UICollectionViewLayoutAttributes] = [:]
    
    override func prepare() {
        
        guard let collectionView = collectionView else { return }
        
        // Only calculate if the cache is empty
        guard cellLayoutCache.isEmpty else { return }
        
        let sections = 0 ... collectionView.numberOfSections - 1
        
        // Set the content height as the initial Y offset
        // so that the second cell overlaps the first
        contentHeight = initialYOffset
        
        // Loop through all the sections
        for section in sections {
            
            let itemsInSection = collectionView.numberOfItems(inSection: section)
            
            // Generate valid index paths for all items in the section
            let indexPaths = [Int](0 ... itemsInSection - 1).map {
                IndexPath(item: $0, section: section)
            }
            
            // Loop through all index paths and cache all the layout attributes
            // so it can be reused later
            for indexPath in indexPaths {
                
                let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                var itemFrame: CGRect
                
                // Check if it is the first item
                if attribute.indexPath.item == 0 {
                    
                    itemFrame = CGRect(x: 0,
                                       y: 0,
                                       width: itemSize.width,
                                       height: itemSize.height)
                }
                else {
                    
                    // Use the current content height as the y position
                    // so the items form one column
                    itemFrame = CGRect(x: 20,
                                       y: contentHeight,
                                       width: itemSize.width,
                                       height: itemSize.height)
                    
                    // Update the y offset by the cell height and spacing
                    contentHeight += itemSize.height + cellSpacing
                }
                
                // Set the frame for the current item
                attribute.frame = itemFrame
                
                cellLayoutCache[indexPath] = attribute
            }
        }
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        
        // Retrieve the attributes from the cache
        return cellLayoutCache[indexPath]
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        
        // Get the attributes that fall in the current view port
        let itemAttributes
            = cellLayoutCache.values.filter { rect.intersects($0.frame) }
        
        return itemAttributes
    }
    
    override var collectionViewContentSize: CGSize {
        guard let collectionView = collectionView else { return .zero }
        
        // Update the content size with the new height
        return CGSize(width: collectionView.bounds.width,
                      height: contentHeight)
    }
}

This seems to work ok for the most part but if I scroll down and come back,the first (red) cell comes in front:

Custom UICollectionViewLayout iOS Swift overlap cells

So then I just add the z index in prepare function to solve this

// Check if it is the first item
if attribute.indexPath.item == 0 {
    
    itemFrame = CGRect(x: 0,
                       y: 0,
                       width: itemSize.width,
                       height: itemSize.height)
    
    attribute.zIndex = -1
}
else {
    
    // Use the current content height as the y position
    // so the items form one column
    itemFrame = CGRect(x: 20,
                       y: contentHeight,
                       width: itemSize.width,
                       height: itemSize.height)
    
    // Update the y offset by the cell height and spacing
    contentHeight += itemSize.height + cellSpacing
    
    attribute.zIndex = 0
}

And now everything works as it should

UICollectionViewFlowLayout iOS Swift overlapping cells

The full example of this code can be found here

Solution 2:[2]

You should create your own UICollectionViewFlowLayout subclass and use the following method to set attributes:

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let attributes = super.layoutAttributesForElements(in: rect)
    let backMostCellIndex = 0
    
    guard  let attributes = attributes else {
        return nil
    }

    for attribute in attributes {
        if attribute.indexPath.item == backMostCellIndex {
            attribute.zIndex = 0
        } else {
            attribute.zIndex = 1
        }
    }
    return attributes
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Shawn Frank
Solution 2 Alex Shpilenia