Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionView custom line separators

Tags:

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

like image 489
Anton Gaenko Avatar asked Feb 24 '15 08:02

Anton Gaenko


2 Answers

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.

like image 178
Anton Gaenko Avatar answered Oct 23 '22 22:10

Anton Gaenko


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 } 
like image 33
Slavik Voloshyn Avatar answered Oct 23 '22 22:10

Slavik Voloshyn