16

I have a horizontal UICollectionView. I want to make the cells overlap each other by a certain amount of 60 pixels so that the second cells overlaps the first by 60 pixels and the third overlaps the second by the same amount and so on.

I tried sub classing UICollectionViewFlowLayout. But I am not able to place the UICollectionviewCell on top of one another.

Amanda
  • 161
  • 1
  • 1
  • 3

5 Answers5

11

Starting with this tutorial: https://web-beta.archive.org/web/20151116053251/http://blog.tobiaswiedenmann.com/post/35135290759/stacklayout

I modified the stacklayout specifically for a horizontal UICollectionView

UICollectionViewStackLayout.h

#import <UIKit/UIKit.h>

@interface UICollectionViewStackLayout : UICollectionViewFlowLayout

#define STACK_OVERLAP 60 //this corresponds to 60 points which is probably what you want, not pixels
#define ITEM_SIZE CGSizeMake(190,210)

@end

UICollectionViewStackLayout.m

#import "UICollectionViewStackLayout.h"

@implementation UICollectionViewStackLayout

-(id)init{

    self = [super init];

    if (self) {
        [self commonInit];
    }

    return self;
}

-(id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super init]) {
        [self commonInit];
    }

    return self;
}

-(void)commonInit
{
    self.itemSize = ITEM_SIZE;

    //set minimum layout requirements
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    self.minimumInteritemSpacing = 0;
}

-(CGSize)collectionViewContentSize
{
    NSInteger xSize = [self.collectionView numberOfItemsInSection:0]
    * self.itemSize.width;
    NSInteger ySize = [self.collectionView numberOfSections]
    * (self.itemSize.height);

    CGSize contentSize = CGSizeMake(xSize, ySize);

    if (self.collectionView.bounds.size.width > contentSize.width)
        contentSize.width = self.collectionView.bounds.size.width;

    if (self.collectionView.bounds.size.height > contentSize.height)
        contentSize.height = self.collectionView.bounds.size.height;

    return contentSize;
}

-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSArray* attributesArray = [super layoutAttributesForElementsInRect:rect];
    int numberOfItems = [self.collectionView numberOfItemsInSection:0];

    for (UICollectionViewLayoutAttributes *attributes in attributesArray) {
        CGFloat xPosition = attributes.center.x;
        CGFloat yPosition = attributes.center.y;

        if (attributes.indexPath.row == 0) {
            attributes.zIndex = INT_MAX; // Put the first cell on top of the stack
        } else {
            xPosition -= STACK_OVERLAP * attributes.indexPath.row;
            attributes.zIndex = numberOfItems - attributes.indexPath.row; //Other cells below the first one
        }

        attributes.center = CGPointMake(xPosition, yPosition);
    }

    return attributesArray;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path {
    UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
    return attributes;
}

@end
Cœur
  • 37,241
  • 25
  • 195
  • 267
mdewitt
  • 762
  • 6
  • 13
  • i implement your answer in apple tv app. but i am facing issue like first cell is perfect but other all cell overlap when i move to focus next cells, can you please give me a perfect solution if i moved focus result should be same like first cell. – Ali Raza Aug 29 '19 at 08:45
6

I used mdewitt's answer, converted it to swift 3 and i works great! For anyone interested:

class CollectionViewOverlappingLayout: UICollectionViewFlowLayout {

var overlap: CGFloat = 30

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    self.scrollDirection = .horizontal
    self.minimumInteritemSpacing = 0
}

override var collectionViewContentSize: CGSize{
    let xSize = CGFloat(self.collectionView!.numberOfItems(inSection: 0)) * self.itemSize.width
    let ySize = CGFloat(self.collectionView!.numberOfSections) * self.itemSize.height

    var contentSize = CGSize(width: xSize, height: ySize)

    if self.collectionView!.bounds.size.width > contentSize.width {
        contentSize.width = self.collectionView!.bounds.size.width
    }

    if self.collectionView!.bounds.size.height > contentSize.height {
        contentSize.height = self.collectionView!.bounds.size.height
    }

    return contentSize
}

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

    let attributesArray = super.layoutAttributesForElements(in: rect)
    let numberOfItems = self.collectionView!.numberOfItems(inSection: 0)

    for attributes in attributesArray! {
        var xPosition = attributes.center.x
        let yPosition = attributes.center.y

        if attributes.indexPath.row == 0 {
            attributes.zIndex = Int(INT_MAX) // Put the first cell on top of the stack
        } else {
            xPosition -= self.overlap * CGFloat(attributes.indexPath.row)
            attributes.zIndex = numberOfItems - attributes.indexPath.row //Other cells below the first one
        }

        attributes.center = CGPoint(x: xPosition, y: yPosition)
    }

    return attributesArray
}

override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    return UICollectionViewLayoutAttributes(forCellWith: indexPath)
}
}
Maik Fruhner
  • 300
  • 4
  • 14
4

Based on Maik Fruhner's answer. To use:

let someCollectionView = UICollectionView(frame: <frame you want>,
                            collectionViewLayout: CollectionViewOverlappingLayout())

Code for the custom layout:

class CollectionViewOverlappingLayout: UICollectionViewFlowLayout {

  var overlap: CGFloat = 14


  override init() {
    super.init()
    self.sharedInit()
  }
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    self.sharedInit()
  }
  func sharedInit(){
    self.scrollDirection = .horizontal
    self.minimumInteritemSpacing = 0
  }

  override var collectionViewContentSize: CGSize{
    let xSize = CGFloat(self.collectionView!.numberOfItems(inSection: 0)) * self.itemSize.width
    let ySize = CGFloat(self.collectionView!.numberOfSections) * self.itemSize.height

    var contentSize = CGSize(width: xSize, height: ySize)

    if self.collectionView!.bounds.size.width > contentSize.width {
      contentSize.width = self.collectionView!.bounds.size.width
    }

    if self.collectionView!.bounds.size.height > contentSize.height {
      contentSize.height = self.collectionView!.bounds.size.height
    }

    return contentSize
  }

  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

    let attributesArray = super.layoutAttributesForElements(in: rect)
    let numberOfItems = self.collectionView!.numberOfItems(inSection: 0)

    for attributes in attributesArray! {
      var xPosition = attributes.center.x
      let yPosition = attributes.center.y

      if attributes.indexPath.row == 0 {
        attributes.zIndex = Int(INT_MAX) // Put the first cell on top of the stack
      } else {
        xPosition -= self.overlap * CGFloat(attributes.indexPath.row)
        attributes.zIndex = numberOfItems - attributes.indexPath.row //Other cells below the first one
      }

      attributes.center = CGPoint(x: xPosition, y: yPosition)
    }

    return attributesArray
  }

  override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    return UICollectionViewLayoutAttributes(forCellWith: indexPath)
  }
}

Works with swift 5 (and probably 4.x).

spnkr
  • 952
  • 9
  • 18
  • It's blowing up in conjunction with diffable datasource - it seems to want to self.collectionView!.numberOfItems on the dataSource.apply – johndpope Jul 16 '20 at 07:50
3

This doesn't blow up using UICollectionViewDiffableDataSource when calling numberOfItemsInSection)

(N.B. the other solutions fix the collectionViewContentSize / I welcome an edit to fix this without calling numberOfItemsInSection.

    var cellLayout = OverlapLayout()
    cellLayout.scrollDirection = .horizontal
    cellLayout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    cellLayout.itemSize = CGSize(width: 44, height: 44)


class OverlapLayout: UICollectionViewFlowLayout {

    var overlap: CGFloat = 20
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let arr = super.layoutAttributesForElements(in: rect)!
        return arr.map { atts in
            var atts = atts
            if atts.representedElementCategory == .cell {
                let ip = atts.indexPath
                atts = self.layoutAttributesForItem(at: ip)!
            }
            return atts
        }
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let atts = super.layoutAttributesForItem(at: indexPath)!
        var xPosition = atts.center.x
        if indexPath.item == 0 {
            return atts
        }else{
           xPosition -= self.overlap * CGFloat(atts.indexPath.row)
           atts.center = CGPoint(x: xPosition, y: atts.center.y)
           return atts
        }

    }

}

enter image description here

// Requires Snapkit
class BubbleCell: UICollectionViewCell {
    static let ID = "BubbleCell"
    var magicCornerRadius = 22 // make this half the collectionview height
    lazy var bubbleImageView: UIImageView = UIImageView(frame: .zero)

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

        setupViews()

    }

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

    private func setupViews() {

        self.backgroundColor =  .clear
        contentView.backgroundColor = .clear

        // Hero Image
        contentView.addSubview(bubbleImageView)
        bubbleImageView.snp.remakeConstraints { (make) in
            make.edges.equalToSuperview()
        }
        bubbleImageView.layer.cornerRadius = magicCornerRadius
        bubbleImageView.layer.borderColor = UIColor.white.cgColor
        bubbleImageView.layer.borderWidth = 2
        bubbleImageView.clipsToBounds = true
        bubbleImageView.image = kPlaceholderImage
        bubbleImageView.contentMode = .scaleAspectFill

    }

    func configureCell(imageURL: URL) {
      // use third party library to fetch image here / that caches eg. SDWebImage

    }

}
johndpope
  • 5,035
  • 2
  • 41
  • 43
  • I believe this solution isn't finished. If you have many items (>100) and scroll it back and forth, the overlapping will change its direction and have visual bugs. Also, the scrolling distance will be much larger than the content. – surfrider Feb 15 '21 at 12:54
  • as per comments / this was a hack - needs more work. sorry. – johndpope Feb 16 '21 at 04:52
2

I used both answers from above to create an example for Xamarin.iOS:

public class CustomFlowLayout : UICollectionViewFlowLayout
{
    public CustomFlowLayout(nfloat width, nfloat height)
    {
        ItemSize = new CGSize(width, height);
        ScrollDirection = UICollectionViewScrollDirection.Horizontal;
    } 

    public override CGSize CollectionViewContentSize
    {
        get
        {
            var xSize = CollectionView.NumberOfItemsInSection(0) * ItemSize.Width;
            var ySize = CollectionView.NumberOfSections() * ItemSize.Height;

            var contentSize = new CGSize(xSize, ySize);

            if (CollectionView.Bounds.Size.Width > contentSize.Width)
                contentSize.Width = CollectionView.Bounds.Size.Width;

            if (CollectionView.Bounds.Size.Height > contentSize.Height)
                contentSize.Height = CollectionView.Bounds.Size.Height;

            return contentSize;
        }
    }

    public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
    {
        var attributesArray = base.LayoutAttributesForElementsInRect(rect);
        var numberOfItems = CollectionView.NumberOfItemsInSection(0);

        foreach (var attributes in attributesArray)
        {
            var xPosition = attributes.Center.X;
            var yPosition = attributes.Center.Y;

            if (attributes.IndexPath.Row == 0)
                attributes.ZIndex = int.MaxValue; // Put the first cell on top of the stack
            else
            {
                xPosition -= 20 * attributes.IndexPath.Row; //20 - value based on your cell width
                attributes.ZIndex = numberOfItems - attributes.IndexPath.Row; //Other cells below the first one
            }

            attributes.Center = new CGPoint(xPosition, yPosition);
        }

        return attributesArray;
    }
}
Alexandru Stefan
  • 635
  • 1
  • 9
  • 22
  • Did you use this in an Effect? Can you share that code too? – iSpain17 Sep 23 '19 at 15:02
  • Not near my laptop now, but if I remember correctly it would look something like uicollectionview colview = new uicollectionview(); colview.ViewFlowLayout = new CustomFlowLayout(number, number) where numbers are usually proportional with the screen width and height. Does it help you? Tomorrow I can post actual code. What is the problem that you encountered? – Alexandru Stefan Sep 23 '19 at 20:15
  • That would be great. My app crashes silently when I use this code to create an instance for the `UICollectionView.SetCollectionViewLayout(UICollectionViewLayout, bool)` method. – iSpain17 Sep 23 '19 at 21:03
  • Sorry for late response. I used: UICollectionView.CollectionViewLayout = new CustomFlowLayout(); If you use the example I posted and still get an error, won't a try/catch give more insights on why it would crash? – Alexandru Stefan Sep 25 '19 at 04:26
  • I edited out the last override of your answer because it is a recursive infinite call, and even, you override to do nothing other than the base implementation. Now it works perfect, while with that override it crashes instantly without an exception. – iSpain17 Mar 31 '20 at 08:05
  • 1
    Apparently only you can edit it, so please do - so others won't have to figure out the same thing that I had to. – iSpain17 Mar 31 '20 at 13:52
  • 1
    @iSpain17 Thank you for the reminder. I had removed it in my actual implementation, but forgot to update here. – Alexandru Stefan Apr 18 '20 at 10:34