40

I need to show 3 items in a UICollectionView, with paging enabled like this

enter image description here

but I am getting like this enter image description here

I have made custom flow, plus paging is enabled but not able to get what i need. How can i achieve this or which delegate should i look into, or direct me to some link from where i can get help for this scenario.

- (void)awakeFromNib
{
    self.itemSize = CGSizeMake(480, 626);
    self.minimumInteritemSpacing = 112;
    self.minimumLineSpacing = 112;
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    self.sectionInset = UIEdgeInsetsMake(0, 272, 0, 272);
}
Raheel Sadiq
  • 9,847
  • 6
  • 42
  • 54

7 Answers7

71

Edit: Demo link: https://github.com/raheelsadiq/UICollectionView-horizontal-paging-with-3-items

After a lot searching I did it, find the next point to scroll to and disable the paging. In scrollviewWillEndDragging scroll to next cell x.

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{

    float pageWidth = 480 + 50; // width + space

    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, scrollView.contentOffset.y) animated:YES];
}

I also had to make the left and right small and center large, so i did it with transform. The issue was finding the index, so that was very difficult to find.

For transform left and right in this same method use the newTargetOffset

int index = newTargetOffset / pageWidth;

if (index == 0) { // If first index 
    UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index  inSection:0]];

    [UIView animateWithDuration:ANIMATION_SPEED animations:^{
        cell.transform = CGAffineTransformIdentity;
    }];
    cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index + 1  inSection:0]];
    [UIView animateWithDuration:ANIMATION_SPEED animations:^{
        cell.transform = TRANSFORM_CELL_VALUE;
    }];
}else{
    UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
    [UIView animateWithDuration:ANIMATION_SPEED animations:^{
        cell.transform = CGAffineTransformIdentity;
    }];

    index --; // left
    cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
    [UIView animateWithDuration:ANIMATION_SPEED animations:^{
        cell.transform = TRANSFORM_CELL_VALUE;
    }];

    index ++;
    index ++; // right
    cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
    [UIView animateWithDuration:ANIMATION_SPEED animations:^{
        cell.transform = TRANSFORM_CELL_VALUE;
    }];
}

And in cellForRowAtIndex add

if (indexPath.row == 0 && isfirstTimeTransform) { // make a bool and set YES initially, this check will prevent fist load transform
    isfirstTimeTransform = NO;
}else{
    cell.transform = TRANSFORM_CELL_VALUE; // the new cell will always be transform and without animation 
}

Add these two macros too or as u wish to handle both

#define TRANSFORM_CELL_VALUE CGAffineTransformMakeScale(0.8, 0.8)
#define ANIMATION_SPEED 0.2

The end result is

enter image description here

Raheel Sadiq
  • 9,847
  • 6
  • 42
  • 54
  • i will be obliged if we can move to chat section of stackoverflow – iPhone Jan 12 '15 at 11:03
  • would you tell me in which method you have written code of "Transforming left and right" side – iPhone Jan 12 '15 at 11:21
  • I desperately need this kind of functionality. Would you like to help me out? – iPhone Jan 12 '15 at 12:09
  • @iphone Hi, i would love to help u, currently I'm on cell phone n outside, lemme get to a system in an hour or so, plus see my answer in detail, i have explained everything, if still u don't get it wait an hour or so – Raheel Sadiq Jan 12 '15 at 12:22
  • @iPhone I am sorry for the late reply, have you done with it, or invite me on a chat – Raheel Sadiq Jan 12 '15 at 19:05
  • Currently a new task has been assigned to me so I will inform you when I will again come to this task – iPhone Jan 16 '15 at 10:14
  • Hello Raheel, Tomorrow would you able to help me in this issue? – iPhone Jan 19 '15 at 11:41
  • @iPhone here is my skype: raheel_raheel11 – Raheel Sadiq Jan 19 '15 at 12:01
  • can you share this file alone? – karthikeyan Jan 30 '15 at 09:07
  • @karthikeyan I have made a sample project of its working fetch it from here: https://www.dropbox.com/s/slmrfzn8c0746jv/CollectionViewDemo.zip?dl=0 – Raheel Sadiq Feb 01 '15 at 07:06
  • @RaheelSadiq Thank you for your great solution. I have a small issue how can I just slide it once and not twice in iPhone6 and 6 Plus? Because if I slide it from the top right to the left it scroll two pages – Luai Kalkatawi Jul 08 '15 at 11:01
  • @LuaiKalkatawi, iphone 6 and 6 plus have more pixels horizontally, so you will have to calculate the spacing between the cells and cell size more precisely, try different space and cell size in scrollViewWillEndDragging, i would recommend to calculate it in percentage according to different screen widths – Raheel Sadiq Jul 09 '15 at 09:23
  • 1
    Can anyone have solution for iPhone 6 and 6plus too ? i tried this code but not able to set the proper logic as per device size.. – Wolverine Mar 07 '16 at 13:32
  • 1
    @Wolverine Did you get the solution for iPhone 6 and 6 Plus? – JMS Apr 01 '16 at 10:37
  • @JMS & Wolverine what is the issue you are facing, I ran this code on 6Plus its working fine for me, may be i'll look into again. Brief the issue – Raheel Sadiq Apr 01 '16 at 10:55
  • When I ran in iPhone 6 Plus, sometimes the left cell will move completely inside and the center cell's position moves to left. I tried changing many values for pageWidth and space. But I could not find the correct value. – JMS Apr 01 '16 at 11:32
  • @RaheelSadiq I think this issue occurs when I set auto layout constraints to my collection view. Without auto layout my view does not work properly – JMS Apr 04 '16 at 05:21
  • Thank you for providing this demo solution – Nikolay Spassov Jul 15 '16 at 07:51
  • @Wolverine To fix it for all screen sizes, you need to override the insetforSection collectionView delegate method, and return the following: (UIScreen.main.bounds.width / 2) - cellWidth / 2 – vikzilla Aug 14 '16 at 06:32
  • @JMS See above comment – vikzilla Aug 14 '16 at 06:32
  • @RaheelSadiq could we achieve scrolling for two/three elements per one paging depending on velocity? – Olexiy Pyvovarov Oct 23 '16 at 09:15
  • @vikzilla insetforSection returns a UIEdgeInsets? what you wrote returns a float – Faisal Aug 20 '17 at 21:21
  • @vikzilla This worked for me thanks to you `UIEdgeInsetsMake(0, (UIScreen.main.bounds.width / 2) - cellWidth / 2, 0, (UIScreen.main.bounds.width / 2) - cellWidth / 2)` – Faisal Aug 20 '17 at 23:18
20

Part one of @Raheel Sadiq answer in Swift 3, without Transform.

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        let pageWidth: Float = Float(self.collectionView.frame.width / 3) //480 + 50
        // width + space
        let currentOffset: Float = Float(scrollView.contentOffset.x)
        let targetOffset: Float = Float(targetContentOffset.pointee.x)
        var newTargetOffset: Float = 0
        if targetOffset > currentOffset {
            newTargetOffset = ceilf(currentOffset / pageWidth) * pageWidth
        }
        else {
            newTargetOffset = floorf(currentOffset / pageWidth) * pageWidth
        }
        if newTargetOffset < 0 {
            newTargetOffset = 0
        }
        else if (newTargetOffset > Float(scrollView.contentSize.width)){
            newTargetOffset = Float(Float(scrollView.contentSize.width))
        }

        targetContentOffset.pointee.x = CGFloat(currentOffset)
        scrollView.setContentOffset(CGPoint(x: CGFloat(newTargetOffset), y: scrollView.contentOffset.y), animated: true)

    }
Gent
  • 6,215
  • 1
  • 37
  • 40
8

Swift 3.0 Complete Solution based on Raheel Sadiq

var isfirstTimeTransform:Bool = true

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {


    let cell : UICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "YourCustomViewCell", for: indexPath)

    if (indexPath.row == 0 && isfirstTimeTransform) { 
        isfirstTimeTransform = false
    }else{
        cell.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) 
    }

    return cell
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

    return CGSize(width: collectionView.bounds.width/3, height: collectionView.bounds.height)
}

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    // Simulate "Page" Function
    let pageWidth: Float = Float(self.collectionView.frame.width/3 + 20)
    let currentOffset: Float = Float(scrollView.contentOffset.x)
    let targetOffset: Float = Float(targetContentOffset.pointee.x)
    var newTargetOffset: Float = 0
    if targetOffset > currentOffset {
        newTargetOffset = ceilf(currentOffset / pageWidth) * pageWidth
    }
    else {
        newTargetOffset = floorf(currentOffset / pageWidth) * pageWidth
    }
    if newTargetOffset < 0 {
        newTargetOffset = 0
    }
    else if (newTargetOffset > Float(scrollView.contentSize.width)){
        newTargetOffset = Float(Float(scrollView.contentSize.width))
    }

    targetContentOffset.pointee.x = CGFloat(currentOffset)
    scrollView.setContentOffset(CGPoint(x: CGFloat(newTargetOffset), y: scrollView.contentOffset.y), animated: true)

    // Make Transition Effects for cells
    let duration = 0.2
    var index = newTargetOffset / pageWidth;
    var cell:UICollectionViewCell = self.collectionView.cellForItem(at: IndexPath(row: Int(index), section: 0))!
    if (index == 0) { // If first index
        UIView.animate(withDuration: duration, delay: 0.0, options: [ .curveEaseOut], animations: {
            cell.transform = CGAffineTransform.identity
        }, completion: nil)
        index += 1
        cell = self.collectionView.cellForItem(at: IndexPath(row: Int(index), section: 0))!
        UIView.animate(withDuration: duration, delay: 0.0, options: [ .curveEaseOut], animations: {
            cell.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
        }, completion: nil)
    }else{
        UIView.animate(withDuration: duration, delay: 0.0, options: [ .curveEaseOut], animations: {
            cell.transform = CGAffineTransform.identity;
        }, completion: nil)

        index -= 1 // left
        if let cell = self.collectionView.cellForItem(at: IndexPath(row: Int(index), section: 0)) {
            UIView.animate(withDuration: duration, delay: 0.0, options: [ .curveEaseOut], animations: {
                cell.transform = CGAffineTransform(scaleX: 0.8, y: 0.8);
            }, completion: nil)
        }

        index += 1
        index += 1 // right
        if let cell = self.collectionView.cellForItem(at: IndexPath(row: Int(index), section: 0)) {
            UIView.animate(withDuration: duration, delay: 0.0, options: [ .curveEaseOut], animations: {
                cell.transform = CGAffineTransform(scaleX: 0.8, y: 0.8);
            }, completion: nil)
        }
    }

}
Community
  • 1
  • 1
Tomas
  • 866
  • 16
  • 21
7

@raheel-sadiq answer is great but pretty hard to understand, I think. Here's a much readable version, in my opinion:

 func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint,
                                   targetContentOffset: UnsafeMutablePointer<CGPoint>) {

        //minimumLineSpacing and insetForSection are two constants in my code

        //this cell width is for my case, adapt to yours
        let cellItemWidth = view.frame.width - (insetForSection.left + insetForSection.right)
        let pageWidth = Float(cellItemWidth + minimumLineSpacing)

        let offsetXAfterDragging = Float(scrollView.contentOffset.x)
        let targetOffsetX = Float(targetContentOffset.pointee.x)

        let pagesCountForOffset = pagesCount(forOffset: offsetXAfterDragging, withTargetOffset: targetOffsetX, pageWidth: pageWidth)

        var newTargetOffsetX = pagesCountForOffset * pageWidth

        keepNewTargetInBounds(&newTargetOffsetX, scrollView)

        //ignore target
        targetContentOffset.pointee.x = CGFloat(offsetXAfterDragging)

        let newTargetPoint = CGPoint(x: CGFloat(newTargetOffsetX), y: scrollView.contentOffset.y)
        scrollView.setContentOffset(newTargetPoint, animated: true)

        //if you're using pageControl
        pageControl.currentPage = Int(newTargetOffsetX / pageWidth)

    }

    fileprivate func pagesCount(forOffset offset: Float, withTargetOffset targetOffset: Float, pageWidth: Float) -> Float {
        let isRightDirection = targetOffset > offset
        let roundFunction = isRightDirection ? ceilf : floorf
        let pagesCountForOffset = roundFunction(offset / pageWidth)
        return pagesCountForOffset
    }

    fileprivate func keepNewTargetInBounds(_ newTargetOffsetX: inout Float, _ scrollView: UIScrollView) {
        if newTargetOffsetX < 0 { newTargetOffsetX = 0 }
        let contentSizeWidth = Float(scrollView.contentSize.width)
        if newTargetOffsetX > contentSizeWidth { newTargetOffsetX = contentSizeWidth }
    }
Daniel Carlos
  • 268
  • 4
  • 6
  • 1
    Great answer.. But in my case, it is just showing some part of next cell. But not of previous cell. Can you help me on this ? – Sushil Sharma May 30 '18 at 11:11
4

you will have to override targetContentOffsetForProposedContentOffset:withScrollingVelocity: method of the flow layout. This way you snap the stopping point of the scrollview.

-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    CGFloat yOffset = MAXFLOAT;

    CGRect proposedRect;
    proposedRect.origin = proposedContentOffset;
    proposedRect.size = self.collectionView.bounds.size;
    CGPoint proposedCenterPoint = CGPointMake(CGRectGetMidX(proposedRect), CGRectGetMidY(proposedRect)) ;

    NSArray *array = [super layoutAttributesForElementsInRect:proposedRect];

    for (UICollectionViewLayoutAttributes *attributes in array)
    {
        CGFloat newOffset = attributes.center.y - proposedCenterPoint.y;
        if ( fabsf(newOffset) < fabs(yOffset))
        {
            yOffset = newOffset;
        }
    }

    return CGPointMake(proposedContentOffset.x, proposedContentOffset.y + yOffset);
}

Also you will beed to set the sectionInset of the flow layout to center the first cell and the last cell. My example is the height but easy to switch to width.

CGFloat height = (self.collectionView.bounds.size.height / 2.0 ) - (self.itemSize.height / 2.0) ;
self.sectionInset = UIEdgeInsetsMake(height, 30.0, height, 30.0) ;
John
  • 2,640
  • 1
  • 16
  • 16
  • I also found this methods, its not helping, the proposedContentOffset.x is always multiple of 1024(ipad), which is not helping, I need 2nd cell on, lets first cell is on x = 272 and spacing is 112 and its width it needs to be at 864. But i am not getting index so i might able to calculate, any other idea or improve this one – Raheel Sadiq Apr 29 '14 at 13:55
  • and yes i have set section inset, i have attached custom flow in question – Raheel Sadiq Apr 29 '14 at 13:56
  • 2
    with paging turned on, the the scroll view calculated the pages. If you want the cell to be in the center, you have to turn off paging. Then calc the closest cell to the center and return that point. – John Apr 29 '14 at 18:54
3

Mine solution to horizontal collection view paging

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    if scrollView == collectionView { collectionView.scrollToPage() }
}

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if scrollView == collectionView { if !decelerate { collectionView.scrollToPage() } }
}

And small extension to collectionView

public func scrollToPage() {
        var currentCellOffset = contentOffset
        currentCellOffset.x += width / 2
        var path = indexPathForItem(at: currentCellOffset)
        if path.isNil {
            currentCellOffset.x += 15
            path = indexPathForItem(at: currentCellOffset)
        }
        if path != nil {
            logInfo("Scrolling to page \(path!)")
            scrollToItem(at: path!, at: .centeredHorizontally, animated: true)
        }
    }
Renetik
  • 5,887
  • 1
  • 47
  • 66
  • hi,why are we adding currentCellOffset.x += 15? – Ishika Mar 20 '20 at 09:50
  • @Ishika Sorry its not nice code to me too, there is some reason definitely I use it still works fine but why I put it there... If you remove and it works write it here ;) Next time hopefully I will write a comment for me too or better named variables so it makes more sense... – Renetik Mar 20 '20 at 14:21
  • @Ishika Ou I think I understand now. It's because you want index path of next item from middle of the view... so exact half is troubled... I am not sure if there should be not -15 when scrolling backwords the but that would make whole solution more complicated. – Renetik Mar 20 '20 at 14:27
0

I am having collection view cell with leading and trailing padding of 15px and was facing paging issue, so I resolved it overriding scrollViewWillEndDragging. You can use below function as:

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    
    let pageWidth: Float = Float(UIScreen.main.bounds.size.width)

    let currentOffset = Float(scrollView.contentOffset.x)
    let targetOffset: Float = Float(targetContentOffset.pointee.x)
    var newTargetOffset: Float = 0

    if targetOffset > currentOffset {
        newTargetOffset = ceilf(currentOffset / pageWidth) * pageWidth
    } else {
        newTargetOffset = floorf(currentOffset / pageWidth) * pageWidth
    }

    if newTargetOffset < 0 {
        newTargetOffset = 0
    } else if CGFloat(newTargetOffset) > scrollView.contentSize.width {
        newTargetOffset = Float(scrollView.contentSize.width)
    }

    targetContentOffset.pointee.x = CGFloat(currentOffset)
    let index = Int(newTargetOffset / pageWidth)
    if index != 0 {
        let spacingForCell:Float = 15
        scrollView.setContentOffset(CGPoint(x: CGFloat( newTargetOffset - spacingForCell*Float(index)), y: 0), animated: true)
    } else {
        scrollView.setContentOffset(CGPoint(x: CGFloat(newTargetOffset), y: 0), animated: true)
    }
}