14

I am using UICollectionView in my UIViewController.

My collectionview properties are set as below.

enter image description here

Now I would like cell to be Centre on screen after scroll! Option 1:

enter image description here

Option 2: enter image description here

What would I have to do achieve option 2?

UPDATE:

In the end I have used following code as scrolling with other answer is not smooth.

  - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{    CGFloat offsetAdjustment = MAXFLOAT;
    CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0);

    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) {
        CGFloat itemHorizontalCenter = layoutAttributes.center.x;

        if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offsetAdjustment)) {
            offsetAdjustment = itemHorizontalCenter - horizontalCenter;
        }
    }    
    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
Nitya
  • 849
  • 1
  • 11
  • 25

8 Answers8

15

You can override targetContentOffsetForProposedContentOffset:withScrollingVelocity: method in your UICollectionViewLayout subclass and calculate your offset there like this:

@property (nonatomic, assign) CGFloat previousOffset;
@property (nonatomic, assign) NSInteger currentPage;

...

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
    NSInteger itemsCount = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:0];

    // Imitating paging behaviour
    // Check previous offset and scroll direction
    if ((self.previousOffset > self.collectionView.contentOffset.x) && (velocity.x < 0.0f)) {
        self.currentPage = MAX(self.currentPage - 1, 0);
    } else if ((self.previousOffset < self.collectionView.contentOffset.x) && (velocity.x > 0.0f)) {
        self.currentPage = MIN(self.currentPage + 1, itemsCount - 1);
    }

    // Update offset by using item size + spacing
    CGFloat updatedOffset = (self.itemSize.width + self.minimumInteritemSpacing) * self.currentPage;
    self.previousOffset = updatedOffset;

    return CGPointMake(updatedOffset, proposedContentOffset.y);
}

EDIT: thanks for pointing this out, forgot to say that you have to disable paging first:

self.collectionView.pagingEnabled = NO;

UPDATE: attaching Swift 4.2 version

...
collectionView.isPagingEnabled = false
...

class YourCollectionLayoutSubclass: UICollectionViewFlowLayout {

    private var previousOffset: CGFloat = 0
    private var currentPage: Int = 0

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        guard let collectionView = collectionView else {
            return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        }

        let itemsCount = collectionView.numberOfItems(inSection: 0)

        // Imitating paging behaviour
        // Check previous offset and scroll direction
        if previousOffset > collectionView.contentOffset.x && velocity.x < 0 {
            currentPage = max(currentPage - 1, 0)
        } else if previousOffset < collectionView.contentOffset.x && velocity.x > 0 {
            currentPage = min(currentPage + 1, itemsCount - 1)
        }

        // Update offset by using item size + spacing
        let updatedOffset = (itemSize.width + minimumInteritemSpacing) * CGFloat(currentPage)
        previousOffset = updatedOffset

        return CGPoint(x: updatedOffset, y: proposedContentOffset.y)
    }
}
Dmitry Zhukov
  • 1,809
  • 2
  • 27
  • 35
11

you can use code self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)

Quach Tam
  • 119
  • 1
  • 2
6

Here's a Swift 3 version of @dmitry-zhukov (thanks btw!)

class PagedCollectionLayout : UICollectionViewFlowLayout {

    var previousOffset : CGFloat = 0
    var currentPage : CGFloat = 0

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

        let sup = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)

        guard
            let validCollection = collectionView,
            let dataSource = validCollection.dataSource
            else { return sup }

        let itemsCount = dataSource.collectionView(validCollection, numberOfItemsInSection: 0)

        // Imitating paging behaviour
        // Check previous offset and scroll direction
        if  (previousOffset > validCollection.contentOffset.x) && (velocity.x < 0) {
            currentPage = max(currentPage - 1, 0)
        }
        else if (previousOffset < validCollection.contentOffset.x) && (velocity.x > 0) {
            currentPage = min(currentPage + 1, CGFloat(itemsCount - 1))
        }

        // Update offset by using item size + spacing
        let updatedOffset = ((itemSize.width + minimumInteritemSpacing) * currentPage)
        self.previousOffset = updatedOffset

        let updatedPoint = CGPoint(x: updatedOffset, y: proposedContentOffset.y)

        return updatedPoint
    }
}
Rici
  • 1,014
  • 2
  • 12
  • 21
3

I have found a lot of information and solutions.

now, I use this.

on UICollectionViewFlowLayout override:

override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    if display != .inline {
        return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
    }

    guard let collectionView = collectionView else {
        return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
    }

    let willtoNextX: CGFloat

    if proposedContentOffset.x <= 0 || collectionView.contentOffset == proposedContentOffset {
        willtoNextX = proposedContentOffset.x
    } else {

        let width = collectionView.bounds.size.width
        willtoNextX = collectionView.contentOffset.x + (velocity.x > 0 ?  width : -width)
    }

    let targetRect = CGRect(x: willtoNextX, y: 0, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height)

    var offsetAdjustCoefficient = CGFloat.greatestFiniteMagnitude

    let horizontalOffset = proposedContentOffset.x + collectionView.contentInset.left

    let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect)

    layoutAttributesArray?.forEach({ (layoutAttributes) in
        let itemOffset = layoutAttributes.frame.origin.x
        if fabsf(Float(itemOffset - horizontalOffset)) < fabsf(Float(offsetAdjustCoefficient)) {
            offsetAdjustCoefficient = itemOffset - horizontalOffset
        }
    })

    return CGPoint(x: proposedContentOffset.x + offsetAdjustCoefficient, y: proposedContentOffset.y)
}

and on UICollectionViewController:

collectionView.decelerationRate = .fast
collectionView.isPagingEnabled = false
collectionView.contentInset = UIEdgeInsets.init(top: 0, left: 16, bottom: 0, right: 16)

now, cell is in center!!

preview

sdo
  • 71
  • 4
3

After setting proper itemSize, left and right insets I prefer doing this rather than subclassing layout

//Setting decelerationRate to fast gives a nice experience
collectionView.decelerationRate = .fast

//Add this to your view anywhere
func centerCell () {
    let centerPoint = CGPoint(x: collectionView.contentOffset.x + collectionView.frame.midX, y: 100)
    if let path = collectionView.indexPathForItem(at: centerPoint) {
        collectionView.scrollToItem(at: path, at: .centeredHorizontally, animated: true)
    }
}

//Set collectionView.delegate = self then add below funcs
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
   centerCell()
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
    centerCell()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if !decelerate {
        centerCell()
    }
}
Husam
  • 8,149
  • 3
  • 38
  • 45
0

I don't know why everybody's answer is so complicated, I simply turn on Paging enabled in Interface Builder and it works perfectly.

Bright
  • 5,699
  • 2
  • 50
  • 72
  • 1
    For none interface builder you can simply set using `isPagingEnabled` on a `UICollectionView` object. – lpdavis13 May 23 '17 at 19:27
  • This is correct, isPagingEnabled is a UIScrollView property which can be set through the interface builder. https://developer.apple.com/documentation/uikit/uiscrollview/1619432-ispagingenabled – Taichi Kato Jul 19 '17 at 09:16
  • 4
    The reason other people's answers are so complicated in contrast to yours is that `isPagingEnabled` makes the collectionView scroll across its entire bounds: in the question you can clearly see that the querent wants to scroll through each item and keep them centred, but not across the entire bounds of the collection view. (Which is the behaviour indicated in the *Option 1* screenshots.) – royalmurder Dec 19 '17 at 09:20
0

My solution for scrolling like paging.

//
collectionview.isPaging = false
//

class CollectionLayoutSubclass: UICollectionViewFlowLayout {

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        var point = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        guard let collectionView = collectionView else {
            return point
        }
        let cells = collectionView.visibleCells
        let centerPoint = collectionView.center
        var cellFrame: CGRect = CGRect.zero
        for cell in cells {
            cellFrame = collectionView.convert(cell.frame, to: collectionView.superview)
            var newCenterPoint: CGPoint = centerPoint
            if velocity.x > 0 {
                newCenterPoint = CGPoint(x: centerPoint.x * 1.5, y: centerPoint.y)
            } else if velocity.x < 0 {
                newCenterPoint = CGPoint(x: centerPoint.x * 0.5, y: centerPoint.y)
            }
            guard cellFrame.contains(newCenterPoint) else {
                continue
            }
            let x = collectionView.frame.width / 2 - cell.frame.width / 2
            point.x = cell.frame.origin.x - x
            break
        }
        return point
    }
}
0

Copied from an answer from another question:

Here is my implementation

func snapToNearestCell(scrollView: UIScrollView) {
     let middlePoint = Int(scrollView.contentOffset.x + UIScreen.main.bounds.width / 2)
     if let indexPath = self.collectionView.indexPathForItem(at: CGPoint(x: middlePoint, y: 0)) {
          self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
     }
}

Implement your scroll view delegates like this

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    self.snapToNearestCell(scrollView: scrollView)
}

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

Also, for better snapping

self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast
Zaporozhchenko Oleksandr
  • 4,660
  • 3
  • 26
  • 48