UICollectionView custom line separators

I started with three ideas how to make it:

  • implement these separators right inside the cells
  • use solid black background with minimumLineSpacing, thus we will see background in spaces between cells
  • use custom layout and implement this separators as decorations

First two variants were rejected because ideologic inconsistency, custom animations and having content below collection. Also I already have a custom layout.

I will describe the steps with a custom subclass of UICollectionViewFlowLayout.

--1--

Implement custom UICollectionReusableView subclass.

@interface FLCollectionSeparator : UICollectionReusableView

@end

@implementation FLCollectionSeparator

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor blackColor];
    }

    return self;
}

- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    self.frame = layoutAttributes.frame;
}

@end

--2--

Say layout to use custom decorations. Also make line spacing between cells.

UICollectionViewFlowLayout* layout = (UICollectionViewFlowLayout*) self.newsCollection.collectionViewLayout;
[layout registerClass:[FLCollectionSeparator class] forDecorationViewOfKind:@"Separator"];
layout.minimumLineSpacing = 2;

--3--

In custom UICollectionViewFlowLayout subclass we should return UICollectionViewLayoutAttributes for decorations from layoutAttributesForElementsInRect.

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    ... collect here layout attributes for cells ... 

    NSMutableArray *decorationAttributes = [NSMutableArray array];
    NSArray *visibleIndexPaths = [self indexPathsOfSeparatorsInRect:rect]; // will implement below

    for (NSIndexPath *indexPath in visibleIndexPaths) {
        UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForDecorationViewOfKind:@"Separator" atIndexPath:indexPath];
        [decorationAttributes addObject:attributes];
    }

    return [layoutAttributesArray arrayByAddingObjectsFromArray:decorationAttributes];
}

--4--

For visible rect we should return visible decorations index pathes.

- (NSArray*)indexPathsOfSeparatorsInRect:(CGRect)rect {
    NSInteger firstCellIndexToShow = floorf(rect.origin.y / self.itemSize.height);
    NSInteger lastCellIndexToShow = floorf((rect.origin.y + CGRectGetHeight(rect)) / self.itemSize.height);
    NSInteger countOfItems = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:0];

    NSMutableArray* indexPaths = [NSMutableArray new];
    for (int i = MAX(firstCellIndexToShow, 0); i <= lastCellIndexToShow; i++) {
        if (i < countOfItems) {
            [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
        }
    }
    return indexPaths;
}

--5--

Also we should implement layoutAttributesForDecorationViewOfKind.

- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind atIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:decorationViewKind withIndexPath:indexPath];
    CGFloat decorationOffset = (indexPath.row + 1) * self.itemSize.height + indexPath.row * self.minimumLineSpacing;
    layoutAttributes.frame = CGRectMake(0.0, decorationOffset, self.collectionViewContentSize.width, self.minimumLineSpacing);
    layoutAttributes.zIndex = 1000;

    return layoutAttributes;
}

--6--

Sometimes I found that this solution gives visual glitches with decorations appearance, which was fixed with implementing initialLayoutAttributesForAppearingDecorationElementOfKind.

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingDecorationElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)decorationIndexPath {
    UICollectionViewLayoutAttributes *layoutAttributes =  [self layoutAttributesForDecorationViewOfKind:elementKind atIndexPath:decorationIndexPath];
    return layoutAttributes;
}

That's all. Not too much code but done right.


Quick solution in Swift

1. Create CustomFlowLayout.swift file and paste next code

import UIKit

private let separatorDecorationView = "separator"

final class CustomFlowLayout: UICollectionViewFlowLayout {

    override func awakeFromNib() {
        super.awakeFromNib()
        register(SeparatorView.self, forDecorationViewOfKind: separatorDecorationView)
    }

    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 {
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .red
    }

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

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

2. Setup custom flow

In the interface builder select your UICollectionViewFlow and set our new class name CustomFlowLayout

3. Change a separator color

In SeparatorView you can change the color of separator in init

4. Change a height of the separator

You can do it in two different ways

  • In the storyboboard. Change a property Min Spacing for Lines

OR

  • In the code. Set value for minimumLineSpacing

    override func awakeFromNib() {
        super.awakeFromNib()
        register(SeparatorView.self, forDecorationViewOfKind: separatorDecorationView)
        minimumLineSpacing = 2 }