Search code examples
iosuicollectionviewuicollectionviewlayoutuicollectionreusableviewuicollectionviewflowlayout

UICollectionViewLayout: add DecorationView only to specific cells


I've developed a custom CollectionViewLayout which uses DecorationView to show shadows behind the cells.

However, I'd like to add this decoration only to some cells. The UICollectionView is vertical, but it may contain an embedded horizontal UICollectionView inside the cell. The cells with an embedded UICollectionView should not be decorated, as shown on the image:

enter image description here

Here is the code I'm using to add a shadow. The UICollectionViewLayout does not provide a method how to retrieve a cell's class, so it could decide whether to add a shadow or not:

  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let parent = super.layoutAttributesForElements(in: rect)
    guard let attributes = parent, !attributes.isEmpty else {
      return parent
    }

    let sections = attributes.map{$0.indexPath.section}
    let unique = Array(Set(sections))


    // Need to detect, which sections contain an embedded UICollectionView and exclude them from the UNIQUE set


    let backgroundShadowAttributes: [UICollectionViewLayoutAttributes] = unique.compactMap{ section in
      let indexPath = IndexPath(item: 0, section: section)
      return self.layoutAttributesForDecorationView(ofKind: backgroundViewClass.reuseIdentifier(),
                                                    at: indexPath)
    }

    return attributes + backgroundShadowAttributes + separators
  }

Is there any way to conditionally specify, which views should be decorated?


Solution

  • Finished with this code: A protocol to directly ask the DataSource, whether to show a shadow for a particular section:

    protocol SectionBackgroundFlowLayoutDataSource {
      func shouldDisplayBackgroundFor(section: Int) -> Bool
    }
    

    And leverage the protocol in the func layoutAttributesForElements(in rect: CGRect) method:

      override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let parent = super.layoutAttributesForElements(in: rect)
        guard let attributes = parent, !attributes.isEmpty else {
          return parent
        }
    
        attributes.forEach(configureRoundCornersAttributes)
    
        // Display shadows for every section by default
        var sectionsWithShadow = Set(attributes.map{$0.indexPath.section})
        if let dataSource = collectionView?.dataSource as? SectionBackgroundFlowLayoutDataSource {
        // Ask DataSource for sections with shadows, if it supports the protocol
          sectionsWithShadow = sectionsWithShadow.filter{dataSource.shouldDisplayBackgroundFor(section: $0)}
        }
    
        let backgroundShadowAttributes: [UICollectionViewLayoutAttributes] = sectionsWithShadow.compactMap{ section in
          let indexPath = IndexPath(item: 0, section: section)
          return self.layoutAttributesForDecorationView(ofKind: backgroundViewClass.reuseIdentifier(),
                                                        at: indexPath)
        }
    
        return attributes + backgroundShadowAttributes + separators
      }
    

    func shouldDisplayBackgroundFor(section: Int) -> Bool may return faster, than cellForItemAtIndexPath, as it doesn't require full cell configuration.