2
votes

I need to create a UICollectionView with cells of different sizes (1x1, 2x2, 2x1, 3x2.5). I've already code to add cells depending on which size they are using collectionView:layout:sizeForItemAtIndexPath:.

Here is expected result (cells 1 and 3 have to be on the left of cell 2) :
enter image description here

But current result is :
enter image description here

My (ugly) current code is (self.cellSize = screen width / 3) :

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {

    CGFloat size = self.cellSize;

    if (indexPath.item == 0) {
        return CGSizeMake(self.cellSize * 3.f, self.cellSize * 2.5f);
    }

    if (indexPath.item == 1) {
        return CGSizeMake(self.cellSize, self.cellSize);
    }

    if (indexPath.item == 2) {
        return CGSizeMake(self.cellSize * 2.f, self.cellSize * 2.f);
    }

    if (indexPath.item == 3) {
        return CGSizeMake(self.cellSize, self.cellSize);
    }

    if (indexPath.item == 4) {
        return CGSizeMake(self.cellSize * 2.f, self.cellSize);
    }

    if (indexPath.item == 5) {
        return CGSizeMake(self.cellSize, self.cellSize);
    }

    if (indexPath.item == 6) {
        return CGSizeMake(self.cellSize, self.cellSize);
    }

    if (indexPath.item == 7) {
        return CGSizeMake(self.cellSize * 2.f, self.cellSize);
    }

    if (indexPath.item == 8) {
        return CGSizeMake(self.cellSize * 3.f, self.cellSize * 2.5f);
    }

    return CGSizeMake(size, size);
}

Is there a way to specify that cell 3 has to be above cell 1, but on left of cell 2 ?

I don't want to do a static layout that will not change because each cell could be of different size in future. For example, cell 2 could be 2x1 sized ...

Which is the best approach to do that ? I was expecting to specify to UICollectionViewLayout a flow (like "from top"), but it doesn't work like that ...

2
I think that you have to subclass UICollectionViewLayout where you'll give each cells its frame.Larme

2 Answers

10
votes

A sample example by subclassing UICollectionViewLayout. All values are hard coded, just to explicit the logic behind it. Of course, that could be optimized.

@interface CustomCollectionViewLayout ()

@property (nonatomic, strong) NSMutableDictionary *cellLayouts;
@property (nonatomic, assign) CGSize              unitSize;

@end

@implementation CustomCollectionViewLayout


-(id)initWithSize:(CGSize)size
{
  self = [super init];
  if (self)
  {
    _unitSize = CGSizeMake(size.width/3,150);
    _cellLayouts = [[NSMutableDictionary alloc] init];
  }
  return self;
}
-(void)prepareLayout
{

  for (NSInteger i = 0; i < [[self collectionView] numberOfItemsInSection:0]; i ++)
  {
    NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
    UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    CGRect frame;
    switch ([indexPath item])
    {
      case 0:
        frame = CGRectMake(0, 0, _unitSize.width*3, _unitSize.height*2.5);
        break;
      case 1:
        frame = CGRectMake(0, _unitSize.height*2.5, _unitSize.width, _unitSize.height);
        break;
      case 2:
        frame = CGRectMake(_unitSize.width, _unitSize.height*2.5, _unitSize.width*2, _unitSize.height*2);
        break;
      case 3:
        frame = CGRectMake(0, _unitSize.height*2.5+_unitSize.height, _unitSize.width, _unitSize.height);
        break;
      case 4:
        frame = CGRectMake(0, _unitSize.height*2.5+_unitSize.height+_unitSize.height, _unitSize.width*2, _unitSize.height);
        break;
      case 5:
        frame = CGRectMake(_unitSize.width*2, _unitSize.height*2.5+_unitSize.height+_unitSize.height, _unitSize.width, _unitSize.height);
        break;
      case 6:
        frame = CGRectMake(0, _unitSize.height*2.5+_unitSize.height+_unitSize.height+_unitSize.height, _unitSize.width, _unitSize.height);
        break;
      case 7:
        frame = CGRectMake(_unitSize.width, _unitSize.height*2.5+_unitSize.height+_unitSize.height+_unitSize.height, _unitSize.width*2, _unitSize.height);
        break;
      case 8:
        frame = CGRectMake(0, _unitSize.height*2.5+_unitSize.height+_unitSize.height+_unitSize.height+_unitSize.height, _unitSize.width*3, _unitSize.height*2.5);
        break;
      default:
        frame = CGRectZero;
        break;
    }
    [attributes setFrame:frame];
    [[self cellLayouts] setObject:attributes forKey:indexPath];
  }
}

-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
  NSMutableArray *retAttributes = [[NSMutableArray alloc] init];
  for (NSIndexPath *anIndexPath in [self cellLayouts])
  {
    UICollectionViewLayoutAttributes *attributes = [self cellLayouts][anIndexPath];
    if (CGRectIntersectsRect(rect, [attributes frame]))
    {
      [retAttributes addObject:attributes];
    }
  }
  return retAttributes;
}

-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{

  return [self cellLayouts][indexPath];
}

-(CGSize)collectionViewContentSize
{
  return CGSizeMake(_unitSize.width*3, _unitSize.height*9);
}

@end

Then, you just have to call :

CustomCollectionViewLayout *layout = [[CustomCollectionViewLayout alloc] initWithSize:self.myCollectionView.bounds.frame.size];
[self.myCollectionView setCollectionViewLayout:layout];

Rendering : enter image description here

2
votes

Here it's the same logic as Larme suggested. But with less hardcode and which give you the possibility of setting the number of items you want without adding new case:.

I call "pattern" a set of 5 items. So first I define constant values:

#define MAX_COLUMN              3 // Max columns in the pattern
#define MAX_LINE_PER_PATTERN    3 // Max lines in the pattern
#define PATTERN_ITEM_COUNT      5 // Max items in the pattern

Then I create a custom layout with 2 properties NSMutableArray *layoutAttributes and CGFloat contentHeight in which I need to override the methods:

- (void)prepareLayout{
    [super prepareLayout];

    if (!self.layoutAttributes){
        self.layoutAttributes = [[NSMutableArray alloc] init];

        CGFloat cellWidth = self.collectionView.frame.size.width / MAX_COLUMN;
        CGFloat cellHeight = cellWidth;
        self.contentHeight = 0.f;

        for (int item = 0 ; item < [self.collectionView numberOfItemsInSection:0] ; item ++){

            CGFloat width, height = 0.f;
            CGFloat xPos, yPos = 0.f;

            NSInteger patternCount = (NSInteger)((CGFloat)item / (CGFloat)PATTERN_ITEM_COUNT);
            NSInteger currentIndex = item % PATTERN_ITEM_COUNT;
            switch (currentIndex) {
                case 0:
                {
                    xPos = 0.f;
                    yPos = 0.f + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
                    width = cellWidth;
                    height = cellHeight;
                    self.contentHeight += cellHeight;
                    break;
                }
                case 1:
                {
                    xPos = cellWidth;
                    yPos = 0.f + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
                    width = cellWidth * 2.f;
                    height = cellHeight * 2.f;
                    self.contentHeight += cellHeight;
                    break;
                }
                case 2:
                {
                    xPos = 0.f;
                    yPos = cellHeight + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
                    width = cellWidth;
                    height = cellHeight;
                    break;
                }
                case 3:
                {
                    xPos = 0.f;
                    yPos = 2.f * cellHeight + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
                    width = cellWidth * 2.f;
                    height = cellHeight;
                    self.contentHeight += cellHeight;
                    break;
                }
                case 4:
                {
                    xPos = 2.f * cellWidth;
                    yPos = 2.f * cellHeight + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
                    width = cellWidth;
                    height = cellHeight;
                    break;
                }
                default:
                    NSLog(@"error with index");
                    break;
            }
            UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForRow:item inSection:0]];
            attr.frame = CGRectMake(xPos,
                                    yPos,
                                    width,
                                    height);
            [self.layoutAttributes addObject:attr];
        }
    }
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    NSMutableArray *currentAttributes = [NSMutableArray new];
    for (UICollectionViewLayoutAttributes *attr in self.layoutAttributes) {
        if (CGRectIntersectsRect(attr.frame, rect))
        {
            [currentAttributes addObject:attr];
        }
    }
    return currentAttributes;
}

- (CGSize)collectionViewContentSize{
    return CGSizeMake(self.collectionView.frame.size.width, self.contentHeight);
}

Then assign this custom layout to self.collectionView.collectionViewLayout and that's it. You can find more information and swift version here.