0

I'm currently trying to implement custom divider views in between each cell of my collection view. I found this answer online that adds the custom view in the inter-line spacing (link).

private let separatorDecorationView = "separator"

final class CustomFlowLayout: UICollectionViewFlowLayout {
    override init() {
        super.init()
        register(SeparatorView.self,
                 forDecorationViewOfKind: separatorDecorationView)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect) ?? []
        let lineWidth = self.minimumLineSpacing

        var decorationAttributes: [UICollectionViewLayoutAttributes] = []

        // skip first cell
        for layoutAttribute in layoutAttributes where layoutAttribute.indexPath.item > 0 {
            let separatorAttribute = UICollectionViewLayoutAttributes(forDecorationViewOfKind: separatorDecorationView,
                                                                      with: layoutAttribute.indexPath)
            let cellFrame = layoutAttribute.frame
            separatorAttribute.frame = CGRect(x: cellFrame.origin.x,
                                              y: cellFrame.origin.y - lineWidth,
                                              width: cellFrame.size.width,
                                              height: lineWidth)
            separatorAttribute.zIndex = Int.max
            decorationAttributes.append(separatorAttribute)
        }

        return layoutAttributes + decorationAttributes
    }
}

private final class SeparatorView: UICollectionReusableView {
    private let imageView: UIImageView = {
        let iv = UIImageView(image: UIImage(named: "cell-divider"))
        iv.contentMode = .scaleAspectFit
        iv.translatesAutoresizingMaskIntoConstraints = false
        return iv
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(imageView)

        imageView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
        imageView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
        imageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
        self.frame = layoutAttributes.frame
    }
}

This solution actually works, and I'm able to see the dividers. The problem arises, however, when the user clicks on one of the cells. The behavior I want is for the cell to expand to show more details when the cell is clicked. The way I'm implementing this is by keeping track of which indexPaths are selected, and returning a larger size if they are selected in sizeForItemAt. In didSelectItemAt, I reload the collection view. This approach works when I'm using the normal UICollectionViewFlowLayout, but when I try using my custom flow layout (above), I get the following crash:

no UICollectionViewLayoutAttributes instance for -layoutAttributesForDecorationViewOfKind: separator at path <NSIndexPath: 0xf75c5b66b8a0a8ab> {length = 2, path = 0 - 6}

I tried looking up solutions and found these two stack overflows here and here but none of the answers I tried seemed to work.

I tried:

  1. Invalidating the layout when I reload the collection view.
  2. Implementing a cache that I return from when I override layoutAttributesForItem in my custom layout.

Any help would be greatly appreciated at this point!

kbunarjo
  • 1,277
  • 2
  • 11
  • 27

1 Answers1

0

It seems that I had to overwrite two methods in my custom layout:

    override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let layoutAttributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: elementKind,
                                                                with: indexPath)
        return layoutAttributes;
    }

    override func initialLayoutAttributesForAppearingDecorationElement(ofKind elementKind: String, at decorationIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = layoutAttributesForDecorationView(ofKind: elementKind,
                                                           at: decorationIndexPath)
        return attributes
    }

I also needed to keep the layout invalidation call when I reloaded the data.

collectionView.reloadData()
let context = collectionViewLayout.invalidationContext(forBoundsChange: bounds)
context.contentOffsetAdjustment = CGPoint.zero
collectionView.collectionViewLayout.invalidateLayout(with: context)

layoutSubviews()
kbunarjo
  • 1,277
  • 2
  • 11
  • 27