Centered Paging with Preview Cells on UICollectionView

(Built on XCode 8.0 / Swift 3.0 – Code on BitBucket – perfected by Adam Kornfield)

We’ve all seen it: A collection view where a preview of the next cell is visible while the scrolling has a “paging” effect so that it always stops with a cell that is always centred.

Center

This is a simple exercise but interesting because of its use of an obscure function in the UICollectionViewFlowLayout class.

This gets called when the user lifts his finger and the collection view (which is a scroll view) starts decelerating and will return the content offset where the collection view should stop. Normally, its implemented by the superclass but here we need to override it. First however we must make some space for the first cell to scroll to the middle

The last declaration (proposed by tdavidson) will make for a more native feel that is closer to the UIScrollView.

As a tip, there is a way to add a custom UICollectionViewLayout to a UICollectionView from Interface Builder without losing the panel provided by the standard flow layout where you can set the size of the cell and the distance between them. This is done by not setting the layout to custom as below:

Screen Shot 2015-04-23 at 12.32.03

But by changing the class on the layout outlet itself:

Screen Shot 2015-04-23 at 12.36.04

The class is shown below:

The proposed offset is where the collection view would stop without our intervention. We peek into this area by finding its centre as proposedContentOffsetCenterX and examine our currently visible cells to see which one’s centre is closer to the centre of that area.

Center2

For more tutorials on Collection Views

[catlist name=”UICollectionView”]

82 thoughts on “Centered Paging with Preview Cells on UICollectionView

      1. Hi Michael,

        Very nice post. my use case is a bitt different. I have 2.5 visible cells and as the user drags I want the one fully visible on the left to be highlighted (as in may be border is more in size than the rest. The same as AirBnB iOS App when you select map to see places on a map.

        Not sure what values I should play around to get the same effect like AirBNB.

        Cheers,
        adam

  1. hi , i have the same issue . i have a slider which shows part of the next cell .
    the whole collection view width is 375 , and it shows 50 of the next cell. i mean cell.width is 325 .
    i wonder how can i mange paging my collection view to show the next slide and all 50px of the next cell.
    how can i change your numbers to my problem ?

    1. It should work. The exact values are taken by the collection view bounds and the attributes of each cell. There is no hardcoding of anything. Did you try at all?

  2. It doesn’t snap back to the first cell if you scroll it slightly to the left, just several pixels for example.

    In line 42 x-coordinate becomes negative -0.1 or close to this value so it doesn’t snap.

    To make it work line should be:

    return CGPoint(x: round(candidateAttributes!.center.x – halfWidth), y: proposedContentOffset.y)

  3. Thanks for this post. I convert your code to Objective-C code. All works great. And I added some functionality:

    – (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

    NSArray *array = [super layoutAttributesForElementsInRect:rect];

    for (UICollectionViewLayoutAttributes *attributes in array) {

    CGRect frame = attributes.frame;
    float distance = fabs(self.collectionView.contentOffset.x + self.collectionView.contentInset.left – frame.origin.x);
    float scale = 0.90f * MIN(MAX(1 – distance / self.collectionView.bounds.size.width, 0.60), 1);
    attributes.alpha = 0.1 + scale;
    NSLog(@”%f”, scale);
    attributes.transform = CGAffineTransformMakeScale(scale, scale);
    }

    return array;
    }

    – (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return true;
    }

    This block scale left and right item and add some mini opacity to content in cells.

      1. I think you guys are become too spoiled… It is not difficult to port the code in Objective-C. However, someone on this thread has posted ObjC code so please have a look above.

  4. Great stuff and very helpful! Just one comment and a few questions related to this.

    [… questions]

    I realize that these questions are perhaps not in scope with this post but I thought I would ask in case you’ve already figured this out. Thanks!

    1. For your first question, a UICollectionView is a UIScrollView under the hood, so most of the methods you are looking for are found there. When the scroll view settles it calls the:

      -(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

      where you can put your code to execute.

      On your second question, you can save the contentOffset which is a CGPoint and use that in:

      -(void)setContentOffset:(CGPoint)offset animated:(BOOL)animated;

      1. Thank you so much! I was not getting there with scrollToItemAtIndexPath or selectItemAtIndexPath, or by setting manually with UIEdgeInsets. I just tried with setContentOffset and it works like a charm.

  5. I’ve implemented this and its working alright with one major flay. When dragging a small amount from the first item, the collection view will often just stop at a mid-point and not snap back to the first item. This only happens with the first item, all of the others will snap back no matter how tiny of a drag occurs. Any ideas as to why this is happening?

    1. Wow, I cannot believe you are the first to find this out 😉 It was my dirty little secret. I do not know why this is happening no! I’d like to know myself. For the moment you can fake it for artistic reasons of course: Just before the return statement hardcode the value:

      if(proposedContentOffset.x == -(cv.contentInset.left)) {
      return proposedContentOffset
      }

      I have committed the changes on the master repo. Any thoughts are much appreciated.

      1. Hello Michael!

        Did you find a solution for this? I am still struggling with the first cell. It won’t snap to the center no matter what I do. Your trick does work for iphone 6 or higher, but for example for iphone 5 the cell just stays wherever I live it, it doesn’t snap 🙁

        1. Thank you for your observation but I do not at this moment have the time to work on this backwards compatibility problem. If you find a solution I would be very happy to include it in my code. Good luck!

  6. Hey,

    wanted to take a moment to really appreciate what you’ve done here!, I was implementing something similar and tried almost all other approaches but this here was the best solution.

  7. I have my row animating to the last item in the index. After the animation, the last cell is flush on the screen, not in the middle. If I interact, it goes to the middle.

    Any ideas?

  8. Thanks for the article. The major downside of this approach is that it interferes with the perceived velocity when flicking the collection view, i.e. it “brakes” very much.

    This might be improved by taking the velocity into account that is fed into the method. I have better results though with doing the snapping when the scrollview has come to a rest (using [_collectionView scrollToItemAtIndexPath:indexPathForCenterElement atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
    )

    Thanks anyways!

  9. Firstly, I really appreciate this article, it is what I’m trying to do.

    here is the little issue of you example code, I’m not sure is the swift framework had change the rule or what.

    when I was trying to test the code, the compiler complain me that I need not to downcast at line 13

    ” as? [UICollectionViewLayoutAttributes] ”

    I check the doc, and this method is already the [UICollectionViewLayoutAttributes]

    func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?

    So I just unwrapped it, just let you know that.

    cheers!! thanks a lot!

    1. Firstly, thank you for your comments. On the issue you mentioned is must be a change in Swift 2.0. This is about the only piece of code that I have not versioned being so simple so I cannot push an update. I will try and update the post later. Take care.

  10. HI.. Thanks for the very nice article. I want to ask if i want to add some notification in CENTER collectionview cell then where do i need to put that code. I tried to put the code in Layout file but it didn’t work very well. SO anyone who can guide me to achieve this.

    1. Why did it not work? What exactly are you trying to do? Are you trying to send a notification for every move of the collection view? Please describe more and I could help you.

    1. Hello there… I made some changes so please have another look (actually it seemed to be working before but there where some warnings, still I fixed them).

    2. awesome work Michael, works right out the box no matter what size the cell is. Really precise walkthrough! Jan, I ran into that as well. Simply remove the downcast ” as? [UICollectionViewLayoutAttributes]” and the problem should go away. It will be just,
      if let attributesForVisibleCells = self.layoutAttributesForElementsInRect(cvBounds) {…})

  11. Great Solution!

    There’s one caveat though.
    It’s considering items in the opposite direction of the scroll. If the user scroll even just a little bit to the right they still don’t want the current item. So here’s my collaboration. (It’s swift, but shall be easy enough to translate for those who need Obj-C)

    [code removed]

    1. I see what you have done there. I refactored the code as it was almost identical apart from:

      if (attributes.center.x == 0) || (attributes.center.x > (cv.contentOffset.x + halfWidth) && velocity.x < 0) { continue } This smooths the scrolling on the oposite direction as you suggest, although it makes it super sensitive to scrolling. Overall however I like it, so I included your code in the repository.

  12. Hi ,I nee similar app with objective c code ,Can some help where i can get same app with objective c code.Thanks.

  13. Hi,First of all,it was wonderful tutorial for beginner like us.Thanks for sharing.

    I really do have some problem.At your sample project.Try testing with iPhone 5 or lower devices.When scroll pages multiple times left or right.When you scroll to right most which is <page 0,the application crash at that line : http://puu.sh/oYWbT/def2f79802.png

    And another thing is when we added image view inside UICollectionViewCell,I think the image is not center correctly when we set aspect fit.

    Any help please?Thanks

  14. “As a tip, there is a way to add a custom UICollectionViewLayout to a UICollectionView from Interface Builder without losing the panel provided by the standard flow layout where you can set the size of the cell and the distance between them. This is done by not setting the layout to custom as below:”

    I don’t believe it works this way in Xamarin, even after setting it as you instructed via XCode (Xamarin allows you to open files in XCode for more advanced customization).

  15. Awesome…

    Just what I needed now I have adapted it to show 2, 3 and 4 collection cells on a page but without centering them and having the laid out conventionally across the page.

    Great work!

    Many Thanks.

    Gary

  16. Hi! Thank you for this post, it’s awesome!

    But i have a problem with page control. How detected index of cell show right now?

  17. Thanks a lot, It works like a charm. for those who are facing a compiler error at

    if let attributesForVisibleCells = self.layoutAttributesForElementsInRect(cvBounds) as [UICollectionViewLayoutAttributes]

    change the line to

    if let attributesForVisibleCells = self.layoutAttributesForElementsInRect(cvBounds)

  18. Is there Any swift 3 version? there are tones of changes with override methods, and for newbies in programming and swift it’s kind of complicated…

  19. Thanks you! Great work.

    Having an issue though – collectionView is jumping back when flicked a bit (also mentioned by Michael Lauer). Happens only when it needs to return to previous position and velocity != 0.

    Any updates on this?

    1. I have added some code to the bottom that was supposed to fix this… Are you checking out the code on the git repo or copying and pasting? Checking out should be the way…

  20. For some reason when I add your code to my project it would crash when trying to scroll left from the first item. I just used an ‘if let’ statement around the return statement above “// fallback” and it works fine.

    When building your code from BitBucket I didn’t have this issue though so I don’t think it’s your code. Maybe I set something up wonky, not sure.

    1. i am having the same issue when trying to scroll left from first item. how exactly did you implement if let around the return statement?

  21. Hi ! Really nice piece of code. In my app, the collection view wasn’t snapping on the last cell so I had to change last lines of your code :

    var newX = floor(candidateAttributes!.center.x – halfWidth) // This is the variable you were returning
    let maxScroll = cv.contentSize.width – cv.width // The collection view can not scroll more than that point
    newX = min(newX, maxScroll)
    print(“Point \(proposedContentOffset.x) becomes \(newX)”)
    return CGPoint(x: floor(newX), y: proposedContentOffset.y)`

  22. When you scroll to the right, then come back to the first item, then try scroll or bounce past the first item it crashes on this line.

    return CGPoint(x: floor(candidateAttributes!.center.x – halfWidth), y: proposedContentOffset.y)

    Has anyone figured the conversion for this in vertical scrolling? i changed the values but it seems to be a bit off centre and gets worse as you scroll down.

  23. apologies for multiple comments but thought id post solution.

    if you get crash when swiping left from first cell enclose return in instatement.

    if candidateAttributes != nil {
    return CGPoint(x: floor(candidateAttributes!.center.x – halfWidth), y: proposedContentOffset.y)
    }

  24. Hi, great work on this! Works extremely well except for a little issue, if you only slightly swipe from right to left (to go to the next cell on the right), it will very abruptly centre itself instead of scrolling to the right, if you do the same but go from left to right, it works perfectly fine. Do you have any guidance about how this could be fixed? Thanks!

      1. I read the comments before commenting myself and you advised others to checkout the repo instead of copying and pasting, I’ve done that but it still appears to be behaving this way. :/

  25. Hey Michael, first of all: you’re awesome! Thanks for writing this tutorial.

    Question: Is there anyway we can find out what the index is of the cell that is in the middle right now?

      1. I found a way to do it. I added this:

        extension CurrencySelectorTableViewCell: UIScrollViewDelegate{

        func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        self.findCenterIndex()
        }
        }

        and this function:

        func findCenterIndex() {
        let center = self.convert(self.collectionView.center, to: self.collectionView)
        let index = collectionView!.indexPathForItem(at: center)
        if let indexInt = index?.item {
        print(currencies[indexInt])
        }
        }

  26. I’ve 1 more question. Is there any way to format the cell in the middle differently from the ones that are not? E.g. middle cell background == blue whilst the rest background == red?

    1. Nevermind… Got it all working like I want it now. Awesome tutorial dude!

      Here’s my code for formatting the middle cell differently than the other ones:

      let center = self.convert(self.collectionView.center, to: self.collectionView)
      let index = collectionView!.indexPathForItem(at: center)

      let previouslySelectedCell = self.collectionView.cellForItem(at: self.selectedCell) as! CurrencySelectorCollectionViewCell
      previouslySelectedCell.currencyLabel.textColor = UIColor.black
      previouslySelectedCell.currencyLabel.font = UIFont.systemFont(ofSize: 15, weight: UIFontWeightLight)

      if let selectedIndex = index {
      self.selectedCell = selectedIndex
      let selectedCell = self.collectionView.cellForItem(at: selectedIndex) as! CurrencySelectorCollectionViewCell
      selectedCell.currencyLabel.textColor = UIColor.white
      selectedCell.currencyLabel.font = UIFont.systemFont(ofSize: 25, weight: UIFontWeightMedium)
      }

Leave a Reply

Your email address will not be published. Required fields are marked *