24

I'm trying to add UIViews between my UICollectionViewCells in my UICollectionView and I don't know how I could do that. I'm trying to accomplish something like this:

Screenshot

I'll probably need to write a custom UICollectionViewLayout, but I don't really know where to start.

Marco Pompei
  • 1,080
  • 1
  • 8
  • 18

4 Answers4

46

I studied more of how UICollectionViewLayouts work and figured out how to solve it. I have an UICollectionReusableView subclass called OrangeView that will be positioned between my views, than I wrote an UICollectionViewFlowLayout subclass called CategoriesLayout that will deal with my layout.

Sorry for the big block of code, but here is how it looks like:

@implementation CategoriesLayout

- (void)prepareLayout {
    // Registers my decoration views.
    [self registerClass:[OrangeView class] forDecorationViewOfKind:@"Vertical"];
    [self registerClass:[OrangeView class] forDecorationViewOfKind:@"Horizontal"];
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind atIndexPath:(NSIndexPath *)indexPath {
    // Prepare some variables.
    NSIndexPath *nextIndexPath = [NSIndexPath indexPathForItem:indexPath.row+1 inSection:indexPath.section];

    UICollectionViewLayoutAttributes *cellAttributes = [self layoutAttributesForItemAtIndexPath:indexPath];
    UICollectionViewLayoutAttributes *nextCellAttributes = [self layoutAttributesForItemAtIndexPath:nextIndexPath];

    UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:decorationViewKind withIndexPath:indexPath];

    CGRect baseFrame = cellAttributes.frame;
    CGRect nextFrame = nextCellAttributes.frame;

    CGFloat strokeWidth = 4;
    CGFloat spaceToNextItem = 0;
    if (nextFrame.origin.y == baseFrame.origin.y)
        spaceToNextItem = (nextFrame.origin.x - baseFrame.origin.x - baseFrame.size.width);

    if ([decorationViewKind isEqualToString:@"Vertical"]) {
        CGFloat padding = 10;

        // Positions the vertical line for this item.
        CGFloat x = baseFrame.origin.x + baseFrame.size.width + (spaceToNextItem - strokeWidth)/2;
        layoutAttributes.frame = CGRectMake(x,
                                            baseFrame.origin.y + padding,
                                            strokeWidth,
                                            baseFrame.size.height - padding*2);
    } else {
        // Positions the horizontal line for this item.
        layoutAttributes.frame = CGRectMake(baseFrame.origin.x,
                                            baseFrame.origin.y + baseFrame.size.height,
                                            baseFrame.size.width + spaceToNextItem,
                                            strokeWidth);
    }

    layoutAttributes.zIndex = -1;
    return layoutAttributes;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray *baseLayoutAttributes = [super layoutAttributesForElementsInRect:rect];
    NSMutableArray * layoutAttributes = [baseLayoutAttributes mutableCopy];

    for (UICollectionViewLayoutAttributes *thisLayoutItem in baseLayoutAttributes) {
        if (thisLayoutItem.representedElementCategory == UICollectionElementCategoryCell) {
            // Adds vertical lines when the item isn't the last in a section or in line.
            if (!([self indexPathLastInSection:thisLayoutItem.indexPath] ||
                  [self indexPathLastInLine:thisLayoutItem.indexPath])) {
                UICollectionViewLayoutAttributes *newLayoutItem = [self layoutAttributesForDecorationViewOfKind:@"Vertical" atIndexPath:thisLayoutItem.indexPath];
                [layoutAttributes addObject:newLayoutItem];
            }

            // Adds horizontal lines when the item isn't in the last line.
            if (![self indexPathInLastLine:thisLayoutItem.indexPath]) {
                UICollectionViewLayoutAttributes *newHorizontalLayoutItem = [self layoutAttributesForDecorationViewOfKind:@"Horizontal" atIndexPath:thisLayoutItem.indexPath];
                [layoutAttributes addObject:newHorizontalLayoutItem];
            }
        }
    }

    return layoutAttributes;
}

@end

I also wrote a category with some methods to check if an index path is the last in a line, in the last line or the last in a section:

@implementation UICollectionViewFlowLayout (Helpers)

- (BOOL)indexPathLastInSection:(NSIndexPath *)indexPath {
    NSInteger lastItem = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:indexPath.section] -1;
    return  lastItem == indexPath.row;
}

- (BOOL)indexPathInLastLine:(NSIndexPath *)indexPath {
    NSInteger lastItemRow = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:indexPath.section] -1;
    NSIndexPath *lastItem = [NSIndexPath indexPathForItem:lastItemRow inSection:indexPath.section];
    UICollectionViewLayoutAttributes *lastItemAttributes = [self layoutAttributesForItemAtIndexPath:lastItem];
    UICollectionViewLayoutAttributes *thisItemAttributes = [self layoutAttributesForItemAtIndexPath:indexPath];

    return lastItemAttributes.frame.origin.y == thisItemAttributes.frame.origin.y;
}

- (BOOL)indexPathLastInLine:(NSIndexPath *)indexPath {
    NSIndexPath *nextIndexPath = [NSIndexPath indexPathForItem:indexPath.row+1 inSection:indexPath.section];

    UICollectionViewLayoutAttributes *cellAttributes = [self layoutAttributesForItemAtIndexPath:indexPath];
    UICollectionViewLayoutAttributes *nextCellAttributes = [self layoutAttributesForItemAtIndexPath:nextIndexPath];

    return !(cellAttributes.frame.origin.y == nextCellAttributes.frame.origin.y);
}

@end

And this is the final result:

Final Result

Marco Pompei
  • 1,080
  • 1
  • 8
  • 18
  • When a cell is removed from the bottom, this code doesn't remove the separator, is there a way to do that? – Weston Feb 24 '14 at 14:30
  • 1
    @Kronusdark try calling `-invalidateLayout` on the collection view layout to see if it forces redrawing everything. I haven't tested it, not sure if it works… – Marco Pompei Feb 24 '14 at 14:38
  • That works, but it hangs out for a second, I will have to play with the timing methinks. Thank you very much for the quick reply – Weston Feb 24 '14 at 14:40
  • @Kronusdark Yeah, I thought it could happen. I checked the [UICollectionViewLayout Class Reference](https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewLayout_class/Reference/Reference.html), check the _Methods to Override_, six of them concerns adding and removing items, implementing them should work.Maybe I'll try to update this code when I get home, but I can't promise you anything. – Marco Pompei Feb 24 '14 at 14:50
  • Too hard to understand, if there's a source project download link, it'll be better – Gank Nov 05 '14 at 09:52
  • @MarcoPompei Why I can't see the lines? My `OrangeView : UICollectionReusableView` if an empty class. – Gank Nov 05 '14 at 10:07
1

Looks like if your collectionView background was green and contentView white you could get the horizontals with a space between the cells minimumLineSpacing. The vertical gap would be the tricky part, but if you were creative with your contentView and set the minimumInteritemSpacing carefully you could get it.

Samuel
  • 401
  • 2
  • 10
0

If you're using sections and the layout is appropriate for it, you might use section headers and footers.

But based on your illustration, it looks like you just need to define the UICollectionViewCell to contain those views. So, where you register your class:

[collectionView registerClass:[CollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];

put those border images in the UICollectionView cell subclass (in the above case, "CollectionViewCell"). That seems like the easiest approach.

Here's one I use:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        self.restorationIdentifier = @"Cell";
        self.backgroundColor = [UIColor clearColor];
        self.autoresizingMask = UIViewAutoresizingNone;
        const CGFloat borderWidth = 3.0f;
        UIView *bgView = [[UIView alloc] initWithFrame:frame];
        bgView.layer.borderColor = [UIColor blackColor].CGColor;
        bgView.layer.borderWidth = borderWidth;
        bgView.layer.cornerRadius = 6.0f;
        self.selectedBackgroundView = bgView;
    }
    return self;
}
RegularExpression
  • 3,531
  • 2
  • 25
  • 36
  • Sections aren't appropriated. I need to know things like if this is the last cell on this line I shouldn't display the line on the right. I want a Collection View that could be rotated and maybe even reused on the iPad, where the distance between cells would be different. – Marco Pompei Apr 01 '13 at 19:37
  • 1
    You can control the spacing and size of the layouts (regardless of target device or orientation) through the methods of UICollectionViewDelegateFlowLayout protocol. If you structure the cell correctly, there is no reason you can't control the border views in any way you wish based on the indexPath of the cell. – RegularExpression Apr 01 '13 at 23:57
-1

Wow. That's a lot of code in the other answers just for a separator line between rows..

This is how I solved it. First you'll need to add the line separator inside the cell. Make sure you keep dragging it making it wider than the actual cell width so if your cell width is 60p your separator line will be 70.

@implementation CollectionViewController
{
    NSArray *test;
    int currentLocInRow;
}

inside cellForItemAtIndexPath:

if((indexPath.row+1) % 4 == 0)
    {
        cell.clipsToBounds = YES;
    }
    else
    {
        cell.clipsToBounds = NO;
    }
    currentLocInRow++;

    if([test count] - indexPath.row+1 < 4 - currentLocInRow)
    {
        cell.lineSeparator.alpha = 0;
    }
    else
    {
        cell.lineSeparator.alpha = 1;
    }
    if(currentLocInRow==4)currentLocInRow=0;

enter image description here

If you want a separator at the end of the collection view but there's a chance that you won't get 4 cells at the last row you can add a simple Collection Reusable View as Footer.

Segev
  • 19,035
  • 12
  • 80
  • 152
  • The trickiest part is getting separators between items inside the same line and getting it to work independent of the container size (when you rotate or on an iPad). – Marco Pompei Dec 11 '13 at 17:55
  • @MarcoPompei I only needed to implement lines under the rows for the app I was working on. If i'll ever need to add separators between the items I'll post my solution here (if i'll find a better one..) Great question. – Segev Dec 11 '13 at 18:01
  • Although this is less code, it is not something that should be encourage to do. This is obviously a hack that only "solves" 50% of the problem – raulriera Jan 07 '16 at 15:00