UICollectionView align logic missing in horizontal paging scrollview UICollectionView align logic missing in horizontal paging scrollview xcode xcode

UICollectionView align logic missing in horizontal paging scrollview


The fundamental issue is Flow Layout is not designed to support the paging. To achieve the paging effect, you will have to sacrifice the space between cells. And carefully calculate the cells frame and make it can be divided by the collection view frame without remainders. I will explain the reason.

Saying the following layout is what you wanted.

enter image description here

Notice, the most left margin (green) is not part of the cell spacing. It is determined by the flow layout section inset. Since flow layout doesn't support heterogeneous spacing value. It is not a trivial task.

Therefore, after setting the spacing and inset. The following layout is what you will get.

enter image description here

After scroll to next page. Your cells are obviously not aligned as what you expected.

enter image description here

Making the cell spacing 0 can solve this issue. However, it limits your design if you want the extra margin on the page, especially if the margin is different from the cell spacing. It also requires the view frame must be divisible by the cell frame. Sometimes, it is a pain if your view frame is not fixed (considering the rotation case).

The real solution is to subclass UICollectionViewFlowLayout and override following methods

- (CGSize)collectionViewContentSize{    // Only support single section for now.    // Only support Horizontal scroll     NSUInteger count = [self.collectionView.dataSource collectionView:self.collectionView                                               numberOfItemsInSection:0];    CGSize canvasSize = self.collectionView.frame.size;    CGSize contentSize = canvasSize;    if (self.scrollDirection == UICollectionViewScrollDirectionHorizontal)    {        NSUInteger rowCount = (canvasSize.height - self.itemSize.height) / (self.itemSize.height + self.minimumInteritemSpacing) + 1;        NSUInteger columnCount = (canvasSize.width - self.itemSize.width) / (self.itemSize.width + self.minimumLineSpacing) + 1;        NSUInteger page = ceilf((CGFloat)count / (CGFloat)(rowCount * columnCount));        contentSize.width = page * canvasSize.width;    }    return contentSize;}- (CGRect)frameForItemAtIndexPath:(NSIndexPath *)indexPath{    CGSize canvasSize = self.collectionView.frame.size;    NSUInteger rowCount = (canvasSize.height - self.itemSize.height) / (self.itemSize.height + self.minimumInteritemSpacing) + 1;    NSUInteger columnCount = (canvasSize.width - self.itemSize.width) / (self.itemSize.width + self.minimumLineSpacing) + 1;    CGFloat pageMarginX = (canvasSize.width - columnCount * self.itemSize.width - (columnCount > 1 ? (columnCount - 1) * self.minimumLineSpacing : 0)) / 2.0f;    CGFloat pageMarginY = (canvasSize.height - rowCount * self.itemSize.height - (rowCount > 1 ? (rowCount - 1) * self.minimumInteritemSpacing : 0)) / 2.0f;    NSUInteger page = indexPath.row / (rowCount * columnCount);    NSUInteger remainder = indexPath.row - page * (rowCount * columnCount);    NSUInteger row = remainder / columnCount;    NSUInteger column = remainder - row * columnCount;    CGRect cellFrame = CGRectZero;    cellFrame.origin.x = pageMarginX + column * (self.itemSize.width + self.minimumLineSpacing);    cellFrame.origin.y = pageMarginY + row * (self.itemSize.height + self.minimumInteritemSpacing);    cellFrame.size.width = self.itemSize.width;    cellFrame.size.height = self.itemSize.height;    if (self.scrollDirection == UICollectionViewScrollDirectionHorizontal)    {        cellFrame.origin.x += page * canvasSize.width;    }    return cellFrame;}- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{    UICollectionViewLayoutAttributes * attr = [super layoutAttributesForItemAtIndexPath:indexPath];    attr.frame = [self frameForItemAtIndexPath:indexPath];    return attr;}- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{    NSArray * originAttrs = [super layoutAttributesForElementsInRect:rect];    NSMutableArray * attrs = [NSMutableArray array];    [originAttrs enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * attr, NSUInteger idx, BOOL *stop) {        NSIndexPath * idxPath = attr.indexPath;        CGRect itemFrame = [self frameForItemAtIndexPath:idxPath];        if (CGRectIntersectsRect(itemFrame, rect))        {            attr = [self layoutAttributesForItemAtIndexPath:idxPath];            [attrs addObject:attr];        }    }];    return attrs;}

Notice, above code snippet only supports single section and horizontal scroll direction. But it is not hard to expand.

Also, if you don't have millions of cells. Caching those UICollectionViewLayoutAttributes may be a good idea.


You could disable paging on UICollectionView and implement a custom horizontal scrolling/paging mechanism with a custom page width/offset like this:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{    float pageWidth = 210;    float currentOffset = scrollView.contentOffset.x;    float targetOffset = targetContentOffset->x;    float newTargetOffset = 0;    if (targetOffset > currentOffset)        newTargetOffset = ceilf(currentOffset / pageWidth) * pageWidth;    else        newTargetOffset = floorf(currentOffset / pageWidth) * pageWidth;    if (newTargetOffset < 0)        newTargetOffset = 0;    else if (newTargetOffset > scrollView.contentSize.width)        newTargetOffset = scrollView.contentSize.width;    targetContentOffset->x = currentOffset;    [scrollView setContentOffset:CGPointMake(newTargetOffset, 0) animated:YES];}


This answer is way late, but I have just been playing with this problem and found that the cause of the drift is the line spacing. If you want the UICollectionView/FlowLayout to page at exact multiples of your cells width, you must set:

UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *)collectionView.collectionViewLayout;flowLayout.minimumLineSpacing = 0.0;

You wouldn't think the line spacing comes into play in horizontal scrolling, but apparently it does.

In my case I was experimenting with paging left to right, one cell at a time, with no space between cells. Every turn of the page introduced a tiny bit of drift from the desired position, and it seemed to accumulate linearly. ~10.0 pts per turn. I realized 10.0 is the default value of minimumLineSpacing in the flow layout. When I set it to 0.0, no drift, when I set it to half the bounds width, each page drifted an extra half of the bounds.

Changing the minimumInteritemSpacing had no effect.

edit -- from the documentation for UICollectionViewFlowLayout:

@property (nonatomic) CGFloat minimumLineSpacing;

Discussion

...

For a vertically scrolling grid, this value represents the minimum spacing between successive rows. For a horizontally scrolling grid, this value represents the minimum spacing between successive columns. This spacing is not applied to the space between the header and the first line or between the last line and the footer.

The default value of this property is 10.0.