57

I've got a UICollectionView, which works ok, until I start scrolling. Here some pics first: enter image description here

As you can see it's great. As I start scrolling (paging enabled) the first one goes a bit offscreen: enter image description here

This is the problem. Originaly my view have 3 views and I want to scroll and show 3 views only. But as it scrolls (paging enabled) it hides a little bit of the first view and show little bit of the next first view from the next page.

And here is a video, because it's kinda hard to explain: Video of the problem (Dropbox)

Here is a picture of my UICollectionView settings: enter image description here

It's going to be great if someone can help!

Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
Devfly
  • 2,495
  • 5
  • 38
  • 56
  • Maybe my similar question and it's answer might help you! For me it solved the scrolling issue: http://stackoverflow.com/questions/14651464/uicollectoinview-horizontal-scroll-with-inter-item-spacing/14960243#14960243 – Alexander Feb 19 '13 at 14:49
  • Just set `min spacing` to 0 – onmyway133 Sep 30 '15 at 10:53

16 Answers16

78

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.

Ji Fang
  • 3,288
  • 1
  • 21
  • 18
  • 2
    This is a great answer and the only one that works perfectly for me. Thanks. – Rob Keniger Nov 27 '13 at 06:53
  • Really nice answer. For me, making the cell spacing 0 solve my problem. Thanks – Jordan Montel Mar 19 '14 at 09:48
  • This answer plus the answer below is the complete answer to this question. -(CGPoint) targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity – Robert Mao Apr 29 '14 at 04:33
  • 1
    Note: This approach uses the `UICollectionViewFlowLayout`'s `itemSize` property, not `collectionView:layout:sizeForItemAtIndexPath:` – Sam Jun 18 '14 at 22:55
  • I think it's overcomplicated solution – nikolsky Apr 26 '16 at 04:59
  • Saved my day. This method is the key. func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return 0 } – Shobhit C Mar 15 '17 at 05:13
29

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];
}
Leszek Szary
  • 9,763
  • 4
  • 55
  • 62
  • This solution worked for me. Although, the paging animation is not as smooth as standard paging. My setup is a little different. I have two sections, the first section has two cells the second has one cell. The collection view's width is 320px, but each cell is only 216px with no space between each cell and section. I have content inset on my collection view so that that the cells are always in the center of the screen, even the first and last ones. – chazzwozzer Mar 24 '14 at 03:38
  • Worked for me as well, but it isn't very smooth...anyone got any idea on how to make it smoother? – ClockWise Nov 26 '15 at 09:26
20

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.

devdavid
  • 1,571
  • 10
  • 14
  • Thank you for this, I was having the same problem, as I paged to the right, each page became about 10px further to the right. This fixed it and I never would have guessed that line spacing would be the cause. – SeanT Feb 25 '14 at 19:58
  • Thanks!! This can be done in IB as well, in size inspector of the flow layout. – Shaked Sayag Dec 18 '16 at 08:24
9

enter image description here

The solution from the following article is elegant and simple. The main idea is creation the scrollView on top of your collectionView with passing all contentOffset values.

http://b2cloud.com.au/tutorial/uiscrollview-paging-size/

It should be said by implementing this method:

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity;

I didn't achieve a smooth animation like it's happening with pagingEnabled = YES.

nikolsky
  • 193
  • 3
  • 7
  • @Maddy this comment is very old but UICollectionView is a subclass of UIScrollView so this method should be available. – morcutt Jan 19 '16 at 18:21
8

I know this question is old, but for anyone who happens to stumble upon this.

All you have to do to correct this is set the MinimumInterItemSpacing to 0 and decrease the content's frame.

Jonathan
  • 2,623
  • 3
  • 23
  • 38
  • 1
    [flowLayout setMinimumInteritemSpacing:0.0f]; [flowLayout setMinimumLineSpacing:-0.92f] This works for me. – Gaurav Oct 01 '13 at 11:48
  • This won't work if I want some part of next cell to be visible? This would only work if cell width is same as the collectionview width, right? – nr5 Sep 01 '17 at 08:38
7

@devdavid was spot on on the flowLayout.minimumLineSpacing to zero.

It can also be done in the layout editor, setting the Min Spacing for Lines to 0:

Easier Way

erickva
  • 513
  • 6
  • 17
6

I think I understand the problem. I'll try and make you understand it too.

If you look closely, then you will see that this issue happens only gradually and not just on the first page swipe.

enter image description here

If I understand correctly, in your app, currently, every UICollectionView item are those rounded boxes which we see, and you have some offset/margin between all of them which is constant. This is what is causing the issue.

Instead, what you should do, is make a UICollectionView item which is 1/3rd of the width of the whole view, and then add that rounded image view inside it. To refer to the image, the green colour should be your UICollectionViewItem and not the black one.

raz0r
  • 3,372
  • 1
  • 12
  • 11
3

Do you roll your own UICollectionViewFlowLayout?

If so, adding -(CGPoint) targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity will help you to calculate where the scrollview should stop.

This might work (NB: UNTESTED!):

-(CGPoint) targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
                             withScrollingVelocity:(CGPoint)velocity
{
  CGFloat offsetAdjustment = MAXFLOAT;
  CGFloat targetX = proposedContentOffset.x + self.minimumInteritemSpacing + self.sectionInset.left;

  CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);

  NSArray *array = [super layoutAttributesForElementsInRect:targetRect];
  for(UICollectionViewLayoutAttributes *layoutAttributes in array) {

    if(layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
      CGFloat itemX = layoutAttributes.frame.origin.x;

      if (ABS(itemX - targetX) < ABS(offsetAdjustment)) {
        offsetAdjustment = itemX - targetX;
      }
    }
  }

  return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
Audun Kjelstrup
  • 1,430
  • 8
  • 13
  • Tried it, and it's different, but the problem persist! Thanks foe the answer though, any other ideas? – Devfly Nov 13 '12 at 19:43
  • I've implemented and tested this and it works except for one small change: you need to make sure that `proposedContentOffset.x + offsetAdjustment` is not negative otherwise the first element in the collectgion won't center correctly. – Michael Gaylord Dec 09 '15 at 14:20
3

My answer is based on answer https://stackoverflow.com/a/27242179/440168 but is more simple.

enter image description here

You should place UIScrollView above UICollectionView and give them equal sizes:

@property (nonatomic, weak) IBOutlet UICollectionView *collectionView;
@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;

Then configure contentInset of collection view, for example:

CGFloat inset = self.view.bounds.size.width*2/9;
self.collectionView.contentInset = UIEdgeInsetsMake(0, inset, 0, inset);

And contentSize of scroll view:

self.scrollView.contentSize = CGSizeMake(self.placesCollectionView.bounds.size.width*[self.collectionView numberOfItemsInSection:0],0);

Do not forget to set delegate of scroll view:

self.scrollView.delegate = self;

And implement main magic:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if (scrollView == self.scrollView) {
        CGFloat inset = self.view.bounds.size.width*2/9;
        CGFloat scale = (self.placesCollectionView.bounds.size.width-2*inset)/scrollView.bounds.size.width;
        self.collectionView.contentOffset = CGPointMake(scrollView.contentOffset.x*scale - inset, 0);
    }
}
Community
  • 1
  • 1
k06a
  • 17,755
  • 10
  • 70
  • 110
  • hi, just to verify, when you said "You should place UIScrollView above UICollectionView and give them equal sizes:" , you really mean superView.addSubview(collectionView) superView.addSubview(scrollView) right? not scrollView.addSubview(collectionView) – iamdavidlam Jun 13 '15 at 14:45
  • 1
    @iamdavidlam yes, looks like my solution does not support touching collection cells content. I am using it in another manner. Two collection views with their frames one near (under) another. – k06a Jun 13 '15 at 14:51
2

your UICollectionView's width should be an exact multiplication of the cell size width + the left and right insets. In your example, if the cell width is 96, then the UICollectionView's width should be (96 + 5 + 5) * 3 = 318. Or, if you wish to keep UICollectionView's 320 width, your cell size width should be 320 / 3 - 5 - 5 = 96.666.

If this does not help, your UICollectionView's width might be different than what is set in the xib file, when the application runs. To check this - add an NSLog statement to printout the view's size in runtime:

NSLog(@"%@", NSStringFromCGRect(uiContentViewController.view.frame));
gardenofwine
  • 1,344
  • 2
  • 15
  • 24
  • Even though my collection view width is 318, it still does the same thing. – Devfly Nov 18 '12 at 14:12
  • From the video you posted, it looks like you are placing the `UICollectionView` inside a bigger container (a landscape iPhone?). Perhaps that container is resizing the `UICollectionView`? Your `UICollectionView` has an autoresizing mask of `UIViewAutoresizingFlexibleWidth`, and it will resize to fill the width of the container. Can you paste the code you use, or the view hierarchy from the xib file? – gardenofwine Nov 18 '12 at 15:11
1

This is the same problem that I was experiencing and i posted my solution on another post, so I'll post it again here.

I found a solution to it, and it involved subclassing the UICollectionViewFlowLayout.

My CollectionViewCell size is 302 X 457 and i set my minimum line spacing to be 18 (9pix for each cell)

When you extend from that class there are a few methods that need to be over-ridden. One of them is

  • (CGSize)collectionViewContentSize

In this method, I needed to add up the total width of what was in the UICollectionView. That includes the ([datasource count] * widthOfCollectionViewCell) + ([datasource count] * 18)

Here is my custom UICollectionViewFlowLayout methods....

-(id)init
{
    if((self = [super init])){

       self.itemSize = CGSizeMake(302, 457);
       self.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
       self.minimumInteritemSpacing = 0.0f;
       self.minimumLineSpacing = 18.0f;
       [self setScrollDirection:UICollectionViewScrollDirectionHorizontal];
   }
    return self;
}



-(CGSize)collectionViewContentSize{
   return CGSizeMake((numCellsCount * 302)+(numCellsCount * 18), 457);
}

This worked for me, so I hope someone else finds it useful!

bolnad
  • 4,533
  • 3
  • 29
  • 41
1

you also can set view's width to '320+spacing', and then set page enable to yes. it will scroll '320+spacing' for every time. i think it because page enable will scroll view's width but not screen's width.

david
  • 11
  • 2
0

I think I do have a solution for this issue. But I do not if it's the best.

UICollectionViewFlowLayout does contain a property called sectionInset. So you could set the section Inset to whatever your need is and make 1 page equalling one section. Therefore your scrolling should automatically fit properly in the pages ( = sections)

Alexander
  • 7,178
  • 8
  • 45
  • 75
0

I had a similar problem with paging. Even though the cell insets were all 0 and the cell was exactly the same size in width and height as the UICollectionView, the paging wasn't proper.

What I noticed sounds like a bug in this version (UICollectionView, iOS 6): I could see that if I worked with a UICollectionView with width = 310px or above, and a height = 538px, I was in trouble. However, if I decreased the width to, say, 300px (same height) I got things working perfectly!

For some future reference, I hope it helps!

Robert
  • 5,278
  • 43
  • 65
  • 115
Julio Flores
  • 435
  • 4
  • 7
0

I encountered a similar issue when trying to get horizontal paging working on a 3 x 3 grid of cells with section insets and cell & line spacing.

The answer for me (after trying many of the suggestions - including subclassing UICollectionViewFlowLayout and various UIScrollView delegate solutions) was simple. I simply used sections in the UICollectionView by breaking my dataset up into sections of 9 items (or fewer), and utilising the numberOfSectionsInCollectionView and numberOfItemsInSection UICollectionView datasource methods.

The UICollectionView's horizontal paging now works beautifully. I recommend this approach to anyone currently tearing their hair out over a similar scenario.

0

If you're using the default flow-layout for your UICollectionView and do NOT want any space between each cell, you can set its miniumumLineSpacing property to 0 via:

((UICollectionViewFlowLayout *) self.collectionView.collectionViewLayout).minimumLineSpacing = 0;
vikzilla
  • 3,998
  • 6
  • 36
  • 57