UICollectionView align logic missing in horizontal paging scrollview

IphoneIosXcodeCocoa TouchUicollectionview

Iphone Problem Overview


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!

Iphone Solutions


Solution 1 - Iphone

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.

Solution 2 - Iphone

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];
}

Solution 3 - Iphone

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.

Solution 4 - Iphone

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.

Solution 5 - Iphone

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.

Solution 6 - Iphone

@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

Solution 7 - Iphone

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.

Solution 8 - Iphone

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);
}

Solution 9 - Iphone

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);
    }
}

Solution 10 - Iphone

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));

Solution 11 - Iphone

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!

Solution 12 - Iphone

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.

Solution 13 - Iphone

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)

Solution 14 - Iphone

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!

Solution 15 - Iphone

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.

Solution 16 - Iphone

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;

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDevflyView Question on Stackoverflow
Solution 1 - IphoneJi FangView Answer on Stackoverflow
Solution 2 - IphoneLeszek SzaryView Answer on Stackoverflow
Solution 3 - IphonedevdavidView Answer on Stackoverflow
Solution 4 - IphonenikolskyView Answer on Stackoverflow
Solution 5 - IphoneJonathanView Answer on Stackoverflow
Solution 6 - IphoneerickvaView Answer on Stackoverflow
Solution 7 - Iphoneraz0rView Answer on Stackoverflow
Solution 8 - IphoneAudun KjelstrupView Answer on Stackoverflow
Solution 9 - Iphonek06aView Answer on Stackoverflow
Solution 10 - IphonegardenofwineView Answer on Stackoverflow
Solution 11 - IphonebolnadView Answer on Stackoverflow
Solution 12 - IphonedavidView Answer on Stackoverflow
Solution 13 - IphoneAlexanderView Answer on Stackoverflow
Solution 14 - IphoneJulio FloresView Answer on Stackoverflow
Solution 15 - IphoneTapfingerView Answer on Stackoverflow
Solution 16 - IphonevikzillaView Answer on Stackoverflow