iosobjective-cuicollectionviewuicollectionviewlayoutuicollectionreusableview

UICollectionView custom line separators


I wanna making 2pt black separators in UICollectionView for our new app. Screenshot from our app is below. We couldn't use UITableView, because we have custom insert/delete animations, scrolling and parallax effects and so on.

Example


Solution

  • I started with three ideas how to make it:

    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.