Snap to center of a cell when scrolling UICollectionView horizontally - ios
I know some people have asked this question before but they were all about UITableViews or UIScrollViews and I couldn't get the accepted solution to work for me. What I would like is the snapping effect when scrolling through my UICollectionView horizontally - much like what happens in the iOS AppStore. iOS 9+ is my target build so please look at the UIKit changes before answering this.
Thanks.
While originally I was using Objective-C, I since switched so Swift and the original accepted answer did not suffice.
I ended up creating a UICollectionViewLayout subclass which provides the best (imo) experience as opposed to the other functions which alter content offset or something similar when the user has stopped scrolling.
class SnappingCollectionViewLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) }
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x + collectionView.contentInset.left
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height)
let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect)
layoutAttributesArray?.forEach({ (layoutAttributes) in
let itemOffset = layoutAttributes.frame.origin.x
if fabsf(Float(itemOffset - horizontalOffset)) < fabsf(Float(offsetAdjustment)) {
offsetAdjustment = itemOffset - horizontalOffset
}
})
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}
For the most native feeling deceleration with the current layout subclass, make sure to set the following:
collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
Based on answer from Mete and comment from Chris Chute,
Here's a Swift 4 extension that will do just what OP wants. It's tested on single row and double row nested collection views and it works just fine.
extension UICollectionView {
func scrollToNearestVisibleCollectionViewCell() {
self.decelerationRate = UIScrollViewDecelerationRateFast
let visibleCenterPositionOfScrollView = Float(self.contentOffset.x + (self.bounds.size.width / 2))
var closestCellIndex = -1
var closestDistance: Float = .greatestFiniteMagnitude
for i in 0..<self.visibleCells.count {
let cell = self.visibleCells[i]
let cellWidth = cell.bounds.size.width
let cellCenter = Float(cell.frame.origin.x + cellWidth / 2)
// Now calculate closest cell
let distance: Float = fabsf(visibleCenterPositionOfScrollView - cellCenter)
if distance < closestDistance {
closestDistance = distance
closestCellIndex = self.indexPath(for: cell)!.row
}
}
if closestCellIndex != -1 {
self.scrollToItem(at: IndexPath(row: closestCellIndex, section: 0), at: .centeredHorizontally, animated: true)
}
}
}
You need to implement UIScrollViewDelegate protocol for your collection view and then add these two methods:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.collectionView.scrollToNearestVisibleCollectionViewCell()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.collectionView.scrollToNearestVisibleCollectionViewCell()
}
}
Snap to the nearest cell, respecting scroll velocity.
Works without any glitches.
import UIKit
final class SnapCenterLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) }
let parent = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
let itemSpace = itemSize.width + minimumInteritemSpacing
var currentItemIdx = round(collectionView.contentOffset.x / itemSpace)
// Skip to the next cell, if there is residual scrolling velocity left.
// This helps to prevent glitches
let vX = velocity.x
if vX > 0 {
currentItemIdx += 1
} else if vX < 0 {
currentItemIdx -= 1
}
let nearestPageOffset = currentItemIdx * itemSpace
return CGPoint(x: nearestPageOffset,
y: parent.y)
}
}
For what it is worth here is a simple calculation that I use (in swift):
func snapToNearestCell(_ collectionView: UICollectionView) {
for i in 0..<collectionView.numberOfItems(inSection: 0) {
let itemWithSpaceWidth = collectionViewFlowLayout.itemSize.width + collectionViewFlowLayout.minimumLineSpacing
let itemWidth = collectionViewFlowLayout.itemSize.width
if collectionView.contentOffset.x <= CGFloat(i) * itemWithSpaceWidth + itemWidth / 2 {
let indexPath = IndexPath(item: i, section: 0)
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
break
}
}
}
Call where you need it. I call it in
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
snapToNearestCell(scrollView)
}
And
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
snapToNearestCell(scrollView)
}
Where collectionViewFlowLayout could come from:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Set up collection view
collectionViewFlowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
}
SWIFT 3 version of #Iowa15 reply
func scrollToNearestVisibleCollectionViewCell() {
let visibleCenterPositionOfScrollView = Float(collectionView.contentOffset.x + (self.collectionView!.bounds.size.width / 2))
var closestCellIndex = -1
var closestDistance: Float = .greatestFiniteMagnitude
for i in 0..<collectionView.visibleCells.count {
let cell = collectionView.visibleCells[i]
let cellWidth = cell.bounds.size.width
let cellCenter = Float(cell.frame.origin.x + cellWidth / 2)
// Now calculate closest cell
let distance: Float = fabsf(visibleCenterPositionOfScrollView - cellCenter)
if distance < closestDistance {
closestDistance = distance
closestCellIndex = collectionView.indexPath(for: cell)!.row
}
}
if closestCellIndex != -1 {
self.collectionView!.scrollToItem(at: IndexPath(row: closestCellIndex, section: 0), at: .centeredHorizontally, animated: true)
}
}
Needs to implement in UIScrollViewDelegate:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
scrollToNearestVisibleCollectionViewCell()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
scrollToNearestVisibleCollectionViewCell()
}
}
Here is my implementation
func snapToNearestCell(scrollView: UIScrollView) {
let middlePoint = Int(scrollView.contentOffset.x + UIScreen.main.bounds.width / 2)
if let indexPath = self.cvCollectionView.indexPathForItem(at: CGPoint(x: middlePoint, y: 0)) {
self.cvCollectionView.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.cvCollectionView.decelerationRate = UIScrollViewDecelerationRateFast
Works like a charm
I tried both #Mark Bourke and #mrcrowley solutions but they give the pretty same results with unwanted sticky effects.
I managed to solve the problem by taking into account the velocity. Here is the full code.
final class BetterSnappingLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
}
var offsetAdjusment = CGFloat.greatestFiniteMagnitude
let horizontalCenter = proposedContentOffset.x + (collectionView.bounds.width / 2)
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height)
let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect)
layoutAttributesArray?.forEach({ (layoutAttributes) in
let itemHorizontalCenter = layoutAttributes.center.x
if abs(itemHorizontalCenter - horizontalCenter) < abs(offsetAdjusment) {
if abs(velocity.x) < 0.3 { // minimum velocityX to trigger the snapping effect
offsetAdjusment = itemHorizontalCenter - horizontalCenter
} else if velocity.x > 0 {
offsetAdjusment = itemHorizontalCenter - horizontalCenter + layoutAttributes.bounds.width
} else { // velocity.x < 0
offsetAdjusment = itemHorizontalCenter - horizontalCenter - layoutAttributes.bounds.width
}
}
})
return CGPoint(x: proposedContentOffset.x + offsetAdjusment, y: proposedContentOffset.y)
}
}
If you want simple native behavior, without customization:
collectionView.pagingEnabled = YES;
This only works properly when the size of the collection view layout items are all one size only and the UICollectionViewCell's clipToBounds property is set to YES.
Got an answer from SO post here and docs here
First What you can do is set your collection view's scrollview's delegate your class by making your class a scrollview delegate
MyViewController : SuperViewController<... ,UIScrollViewDelegate>
Then make set your view controller as the delegate
UIScrollView *scrollView = (UIScrollView *)super.self.collectionView;
scrollView.delegate = self;
Or do it in the interface builder by control + shift clicking on your collection view and then control + drag or right click drag to your view controller and select delegate. (You should know how to do this). That doesn't work. UICollectionView is a subclass of UIScrollView so you will now be able to see it in the interface builder by control + shift clicking
Next implement the delegate method - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
MyViewController.m
...
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
}
The docs state that:
Parameters
scrollView | The scroll-view object that is decelerating the scrolling
of the content view.
Discussion The scroll view calls this method when the scrolling
movement comes to a halt. The decelerating property of UIScrollView
controls deceleration.
Availability Available in iOS 2.0 and later.
Then inside of that method check which cell was closest to the center of the scrollview when it stopped scrolling
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
//NSLog(#"%f", truncf(scrollView.contentOffset.x + (self.pictureCollectionView.bounds.size.width / 2)));
float visibleCenterPositionOfScrollView = scrollView.contentOffset.x + (self.pictureCollectionView.bounds.size.width / 2);
//NSLog(#"%f", truncf(visibleCenterPositionOfScrollView / imageArray.count));
NSInteger closestCellIndex;
for (id item in imageArray) {
// equation to use to figure out closest cell
// abs(visibleCenter - cellCenterX) <= (cellWidth + cellSpacing/2)
// Get cell width (and cell too)
UICollectionViewCell *cell = (UICollectionViewCell *)[self collectionView:self.pictureCollectionView cellForItemAtIndexPath:[NSIndexPath indexPathWithIndex:[imageArray indexOfObject:item]]];
float cellWidth = cell.bounds.size.width;
float cellCenter = cell.frame.origin.x + cellWidth / 2;
float cellSpacing = [self collectionView:self.pictureCollectionView layout:self.pictureCollectionView.collectionViewLayout minimumInteritemSpacingForSectionAtIndex:[imageArray indexOfObject:item]];
// Now calculate closest cell
if (fabsf(visibleCenterPositionOfScrollView - cellCenter) <= (cellWidth + (cellSpacing / 2))) {
closestCellIndex = [imageArray indexOfObject:item];
break;
}
}
if (closestCellIndex != nil) {
[self.pictureCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathWithIndex:closestCellIndex] atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
// This code is untested. Might not work.
}
A modification of the above answer which you can also try:
-(void)scrollToNearestVisibleCollectionViewCell {
float visibleCenterPositionOfScrollView = _collectionView.contentOffset.x + (self.collectionView.bounds.size.width / 2);
NSInteger closestCellIndex = -1;
float closestDistance = FLT_MAX;
for (int i = 0; i < _collectionView.visibleCells.count; i++) {
UICollectionViewCell *cell = _collectionView.visibleCells[i];
float cellWidth = cell.bounds.size.width;
float cellCenter = cell.frame.origin.x + cellWidth / 2;
// Now calculate closest cell
float distance = fabsf(visibleCenterPositionOfScrollView - cellCenter);
if (distance < closestDistance) {
closestDistance = distance;
closestCellIndex = [_collectionView indexPathForCell:cell].row;
}
}
if (closestCellIndex != -1) {
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:closestCellIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
}
}
This from a 2012 WWDC video for an Objective-C solution. I subclassed UICollectionViewFlowLayout and added the following.
-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
CGFloat offsetAdjustment = MAXFLOAT;
CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2);
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);
}
And the reason I got to this question was for the snapping with a native feel, which I got from Mark's accepted answer... this I put in the collectionView's view controller.
collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
I just found what I think is the best possible solution to this problem:
First add a target to the collectionView's already existing gestureRecognizer:
[self.collectionView.panGestureRecognizer addTarget:self action:#selector(onPan:)];
Have the selector point to a method which takes a UIPanGestureRecognizer as a parameter:
- (void)onPan:(UIPanGestureRecognizer *)recognizer {};
Then in this method, force the collectionView to scroll to the appropriate cell when the pan gesture has ended.
I did this by getting the visible items from the collection view and determining which item I want to scroll to depending on the direction of the pan.
if (recognizer.state == UIGestureRecognizerStateEnded) {
// Get the visible items
NSArray<NSIndexPath *> *indexes = [self.collectionView indexPathsForVisibleItems];
int index = 0;
if ([(UIPanGestureRecognizer *)recognizer velocityInView:self.view].x > 0) {
// Return the smallest index if the user is swiping right
for (int i = index;i < indexes.count;i++) {
if (indexes[i].row < indexes[index].row) {
index = i;
}
}
} else {
// Return the biggest index if the user is swiping left
for (int i = index;i < indexes.count;i++) {
if (indexes[i].row > indexes[index].row) {
index = i;
}
}
}
// Scroll to the selected item
[self.collectionView scrollToItemAtIndexPath:indexes[index] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
}
Keep in mind that in my case only two items can be visible at a time. I'm sure this method can be adapted for more items however.
This solution gives a better and smoother animation.
Swift 3
To get the first and last item to center add insets:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, cellWidth/2, 0, cellWidth/2)
}
Then use the targetContentOffset in the scrollViewWillEndDragging method to alter the ending position.
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let numOfItems = collectionView(mainCollectionView, numberOfItemsInSection:0)
let totalContentWidth = scrollView.contentSize.width + mainCollectionViewFlowLayout.minimumInteritemSpacing - cellWidth
let stopOver = totalContentWidth / CGFloat(numOfItems)
var targetX = round((scrollView.contentOffset.x + (velocity.x * 300)) / stopOver) * stopOver
targetX = max(0, min(targetX, scrollView.contentSize.width - scrollView.frame.width))
targetContentOffset.pointee.x = targetX
}
Maybe in your case the totalContentWidth is calculated differently, f.e. without a minimumInteritemSpacing, so adjust that accordingly.
Also you can play around with the 300 used in the velocity
P.S. Make sure the class adopts the UICollectionViewDataSource protocol
I've been solving this issue by setting 'Paging Enabled' on the attributes inspector on the uicollectionview.
For me this happens when the width of the cell is the same as the width of the uicollectionview.
No coding involved.
Here is a Swift 3.0 version, which should work for both horizontal and vertical directions based on Mark's suggestion above:
override func targetContentOffset(
forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint
) -> CGPoint {
guard
let collectionView = collectionView
else {
return super.targetContentOffset(
forProposedContentOffset: proposedContentOffset,
withScrollingVelocity: velocity
)
}
let realOffset = CGPoint(
x: proposedContentOffset.x + collectionView.contentInset.left,
y: proposedContentOffset.y + collectionView.contentInset.top
)
let targetRect = CGRect(origin: proposedContentOffset, size: collectionView.bounds.size)
var offset = (scrollDirection == .horizontal)
? CGPoint(x: CGFloat.greatestFiniteMagnitude, y:0.0)
: CGPoint(x:0.0, y:CGFloat.greatestFiniteMagnitude)
offset = self.layoutAttributesForElements(in: targetRect)?.reduce(offset) {
(offset, attr) in
let itemOffset = attr.frame.origin
return CGPoint(
x: abs(itemOffset.x - realOffset.x) < abs(offset.x) ? itemOffset.x - realOffset.x : offset.x,
y: abs(itemOffset.y - realOffset.y) < abs(offset.y) ? itemOffset.y - realOffset.y : offset.y
)
} ?? .zero
return CGPoint(x: proposedContentOffset.x + offset.x, y: proposedContentOffset.y + offset.y)
}
Swift 4.2. Simple. For fixed itemSize. Horizontal flow direction.
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
let floatingPage = targetContentOffset.pointee.x/scrollView.bounds.width
let rule: FloatingPointRoundingRule = velocity.x > 0 ? .up : .down
let page = CGFloat(Int(floatingPage.rounded(rule)))
targetContentOffset.pointee.x = page*(layout.itemSize.width + layout.minimumLineSpacing)
}
}
Related
Why collectionView cell centering works only in one direction?
I'm trying to centerelize my cells on horizontal scroll. I've written one method, but it works only when I scroll to right, on scroll on left it just scrolls, without stopping on the cell's center. Can anyone help me to define this bug, please? class CenterCellCollectionViewFlowLayout: UICollectionViewFlowLayout { override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { if let cv = self.collectionView { let cvBounds = cv.bounds let halfWidth = cvBounds.size.width * 0.5; let proposedContentOffsetCenterX = proposedContentOffset.x + halfWidth; if let attributesForVisibleCells = self.layoutAttributesForElementsInRect(cvBounds) { var candidateAttributes: UICollectionViewLayoutAttributes? for attributes in attributesForVisibleCells { // == Skip comparison with non-cell items (headers and footers) == // if attributes.representedElementCategory != UICollectionElementCategory.Cell { continue } if (attributes.center.x == 0) || (attributes.center.x > (cv.contentOffset.x + halfWidth) && velocity.x < 0) { continue } // == First time in the loop == // guard let candAttrs = candidateAttributes else { candidateAttributes = attributes continue } let a = attributes.center.x - proposedContentOffsetCenterX let b = candAttrs.center.x - proposedContentOffsetCenterX if fabsf(Float(a)) < fabsf(Float(b)) { candidateAttributes = attributes; } } if(proposedContentOffset.x == -(cv.contentInset.left)) { return proposedContentOffset } return CGPoint(x: floor(candidateAttributes!.center.x - halfWidth), y: proposedContentOffset.y) } } else { print("else") } // fallback return super.targetContentOffsetForProposedContentOffset(proposedContentOffset) } } And in my UIViewController: override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() var insets = self.collectionView.contentInset let value = ((self.view.frame.size.width - ((CGRectGetWidth(collectionView.frame) - 35))) * 0.5) insets.left = value insets.right = value self.collectionView.contentInset = insets self.collectionView.decelerationRate = UIScrollViewDecelerationRateNormal } If you have any question - please ask me
I actually just did a slightly different implementation for another thread, but adjusted it to work for this questions. Try out the solution below :) /** * Custom FlowLayout * Tracks the currently visible index and updates the proposed content offset */ class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout { // Tracks the currently visible index private var visibleIndex : Int = 0 // The width offset threshold percentage from 0 - 1 let thresholdOffsetPrecentage : CGFloat = 0.5 // This is the flick velocity threshold let velocityThreshold : CGFloat = 0.4 override init() { super.init() self.minimumInteritemSpacing = 0.0 self.minimumLineSpacing = 0.0 self.scrollDirection = .Horizontal } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { let leftThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) - 0.5)) let rightThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) + 0.5)) let currentHorizontalOffset = collectionView!.contentOffset.x // If you either traverse far enough in either direction, // or flicked the scrollview over the horizontal velocity in either direction, // adjust the visible index accordingly if currentHorizontalOffset < leftThreshold || velocity.x < -velocityThreshold { visibleIndex = max(0 , (visibleIndex - 1)) } else if currentHorizontalOffset > rightThreshold || velocity.x > velocityThreshold { visibleIndex += 1 } var _proposedContentOffset = proposedContentOffset _proposedContentOffset.x = CGFloat(collectionView!.bounds.width) * CGFloat(visibleIndex) return _proposedContentOffset } }
Circular wheel animation implementation in ios
I have to do a animation like : https://www.flickr.com/photos/134104584#N07/20409116003/in/datetaken/ Currently I am trying to achieve this using custom collection view layout and subclassing UICollectionViewLayout. Any help or way to achieve this ?
I had the same task and that's what I've found: You can make your own solution, following great tutorial made by Rounak Jain. The tutorial is available here: https://www.raywenderlich.com/107687/uicollectionview-custom-layout-tutorial-spinning-wheel You can reuse the existing code. Because of time limits I've chosen the second variant, and the best repo I've found was: https://github.com/sarn/ARNRouletteWheelView The usage is pretty straightforward, but there are two tips that can be useful: scrollToItemAtIndexPath was not working and was crashing the app. The Pull Request to fix it: https://github.com/sarn/ARNRouletteWheelView/pull/3/commits/7056e4bac2a04f2c8208d6d43e25d62a848f032d If you want to make items smaller and disable rotation of the item itself (see gif), you can do the following in your UICollectionViewCell subclass: #import "ARNRouletteWheelCellScaledAndRotated.h" #import <ARNRouletteWheelView/ARNRouletteWheelLayoutAttributes.h> #implementation ARNRouletteWheelCellScaledAndRotated - (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes { [super applyLayoutAttributes:layoutAttributes]; if(layoutAttributes != nil && [layoutAttributes isKindOfClass:[ARNRouletteWheelLayoutAttributes class]]) { ARNRouletteWheelLayoutAttributes *rouletteWheelLayoutAttributes = (ARNRouletteWheelLayoutAttributes *)layoutAttributes; self.layer.anchorPoint = rouletteWheelLayoutAttributes.anchorPoint; // update the Y center to reflect the anchor point CGPoint center = CGPointMake(self.center.x, self.center.y + ((rouletteWheelLayoutAttributes.anchorPoint.y - 0.5) * CGRectGetHeight(self.bounds))); self.center = center; // rotate back CGAffineTransform rotate = CGAffineTransformMakeRotation(-rouletteWheelLayoutAttributes.angle); // scale CGFloat scale = 1 - fabs(rouletteWheelLayoutAttributes.angle); CGAffineTransform translate = CGAffineTransformMakeScale(scale, scale); // Apply them to a view self.imageView.transform = CGAffineTransformConcat(translate, rotate); } } #end
Update this tutorial - https://www.raywenderlich.com/1702-uicollectionview-custom-layout-tutorial-a-spinning-wheel. Old version not work. This version good work for Swift 5 + i add pageControl if somebody need. import UIKit class CircularCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes { //1 var anchorPoint = CGPoint(x: 0.5, y: 0.5) var angle: CGFloat = 0 { //2 didSet { zIndex = Int(angle * 1000000) transform = CGAffineTransform.identity.rotated(by: angle) } } override func copy(with zone: NSZone? = nil) -> Any { let copiedAttributes: CircularCollectionViewLayoutAttributes = super.copy(with: zone) as! CircularCollectionViewLayoutAttributes copiedAttributes.anchorPoint = self.anchorPoint copiedAttributes.angle = self.angle return copiedAttributes } } protocol CircularViewLayoutDelegate: AnyObject { func getCurrentIndex(value: Int) } class CircularCollectionViewLayout: UICollectionViewLayout { let itemSize = CGSize(width: 214, height: 339) let spaceBeetweenItems: CGFloat = 50 var angleAtExtreme: CGFloat { return collectionView!.numberOfItems(inSection: 0) > 0 ? -CGFloat(collectionView!.numberOfItems(inSection: 0)) * anglePerItem : 0 } var angle: CGFloat { return angleAtExtreme * collectionView!.contentOffset.x / (collectionViewContentSize.width - itemSize.width) } var radius: CGFloat = 1000 { didSet { invalidateLayout() } } var anglePerItem: CGFloat { return atan((itemSize.width + spaceBeetweenItems) / radius) } weak var delegate: CircularViewLayoutDelegate? override var collectionViewContentSize: CGSize { return CGSize(width: (CGFloat(collectionView!.numberOfItems(inSection: 0)) * itemSize.width) + 100, height: collectionView!.bounds.height) } override class var layoutAttributesClass: AnyClass { return CircularCollectionViewLayoutAttributes.self } var attributesList = [CircularCollectionViewLayoutAttributes]() override func prepare() { super.prepare() guard let collectionView = collectionView else { return } let centerX = collectionView.contentOffset.x + collectionView.bounds.width / 2.0 let anchorPointY = (itemSize.height + radius) / itemSize.height // Add to get current cell index let theta = atan2(collectionView.bounds.width / 2.0, radius + (itemSize.height / 2.0) - collectionView.bounds.height / 2.0) let endIndex = collectionView.numberOfItems(inSection: 0) let currentIndex = min(endIndex, Int(ceil((theta - angle) / anglePerItem))) delegate?.getCurrentIndex(value: currentIndex) attributesList = (0..<collectionView.numberOfItems(inSection: 0)).map { (i) -> CircularCollectionViewLayoutAttributes in //1 let attributes = CircularCollectionViewLayoutAttributes.init(forCellWith: IndexPath(item: i, section: 0)) attributes.size = self.itemSize //2 attributes.center = CGPoint(x: centerX, y: (self.collectionView!.bounds).midY) //3 attributes.angle = self.angle + (self.anglePerItem * CGFloat(i)) attributes.anchorPoint = CGPoint(x: 0.5, y: anchorPointY) return attributes } } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { // return an array layout attributes instances for all the views in the given rect return attributesList } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return attributesList[indexPath.row] } override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true } } Function in Cell Class: override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { super.apply(layoutAttributes) let circularlayoutAttributes = layoutAttributes as! CircularCollectionViewLayoutAttributes self.layer.anchorPoint = circularlayoutAttributes.anchorPoint self.center.y += (circularlayoutAttributes.anchorPoint.y - 0.5) * self.bounds.height } Add this to controller if you need current cell to add pageControl: In ViewDidload: if let layout = collectionView?.collectionViewLayout as? CircularCollectionViewLayout { layout.delegate = self } Extensions: extension LevelsVC: CircularViewLayoutDelegate { func getCurrentIndex(value: Int) { self.pageControl.currentPage = value - 1 } }
UICollectionView with Paging Enable
I have to create Grid view like Appstore iOS app. I want to do this with UICollectionView paging. I have also implemented the code but not able to scroll like that. What I want to do is there will one image in Center and at both sides(left and right), it should show some portion of previous and next image. I have set Frame for UICollectionView is 320*320. cell size is 290*320.(cell min spacing is 10)1 Below are two links which depicts my requirement. Thanks in advance. (This is what I want) 2
Have you tried setting the scroll direction of your UICollectionViewFlowLayout to horizontal? [yourFlowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal]; You'll need to enable paging on your collection view like so: [yourCollectionView setPagingEnabled:YES];
I took shtefane's answer and improved on it. Enter your own cellWidth and cellPadding values. - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { CGFloat cellWidth = self.cellWidth; CGFloat cellPadding = 9; NSInteger page = (scrollView.contentOffset.x - cellWidth / 2) / (cellWidth + cellPadding) + 1; if (velocity.x > 0) page++; if (velocity.x < 0) page--; page = MAX(page,0); CGFloat newOffset = page * (cellWidth + cellPadding); targetContentOffset->x = newOffset; }
If you use pagining in collectionView it will scroll by one page Not one cell. You can disable pagining and implement ScrollViewDelegate - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { CGFloat pageW = 300; int page = scrollView.contentOffset.x / pageW; CGFloat newOffset =(page + ((velocity.x > 0)? 1 : -1)) * (pageW - 20); CGPoint target = CGPointMake(newOffset, 0); targetContentOffset = ⌖ NSLog(#"end Drag at %f /%i /%f",scrollView.contentOffset.x, page, velocity.x); } Only one different from standart paging: If you drag fast Collection will scroll more than one cell. And don't forget to add UIScrollViewDelegate
Just use collectionView.isPagingEnabled = true;
Here's a working swift4 version of Rickster answer: func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { let cellWidth = 174 as CGFloat let cellPadding = 10 as CGFloat var page = (scrollView.contentOffset.x - cellWidth / 2) / (cellWidth + cellPadding) + 1 if (velocity.x > 0) { page += 1 } if (velocity.x < 0) { page -= 1 } page = max(page,0) targetContentOffset.pointee.x = page * (cellWidth + cellPadding) }
Ref to Rickster's answer and I rewrite with Swift 4: /* paging */ extension AlbumViewController { /* In case the user scrolls for a long swipe, the scroll view should animate to the nearest page when the scrollview decelerated. */ override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { scrollToPage(scrollView, withVelocity: CGPoint(x:0, y:0)) } override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { scrollToPage(scrollView, withVelocity: velocity) } func scrollToPage(_ scrollView: UIScrollView, withVelocity velocity: CGPoint) { let cellWidth: CGFloat = cellSize let cellPadding: CGFloat = 10 var page: Int = Int((scrollView.contentOffset.x - cellWidth / 2) / (cellWidth + cellPadding) + 1) if velocity.x > 0 { page += 1 } if velocity.x < 0 { page -= 1 } page = max(page, 0) let newOffset: CGFloat = CGFloat(page) * (cellWidth + cellPadding) scrollView.setContentOffset(CGPoint(x:newOffset, y:0), animated: true) } }
full source is here this supports RTL (Right to Left) collectionView.decelerationRate = .fast // paging extension ViewController: UIScrollViewDelegate { func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.dragStartPoint = scrollView.contentOffset } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { let isRTL = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft let pageWidth = UIScreen.main.bounds.size.width - ViewController.left - ViewController.right + ViewController.lineSpacing if scrollView.contentOffset.x == targetContentOffset.pointee.x { // no decelerate if fabsf(Float(self.dragStartPoint.x - scrollView.contentOffset.x)) > 40 { // min move distance = 40 let dragLeft = self.dragStartPoint.x < scrollView.contentOffset.x if dragLeft { self.currentPage = isRTL ? self.currentPage - 1 : self.currentPage + 1 } else { self.currentPage = isRTL ? self.currentPage + 1 : self.currentPage - 1 } } } else if scrollView.contentOffset.x > targetContentOffset.pointee.x { let maxRight = scrollView.contentSize.width - UIScreen.main.bounds.size.width if scrollView.contentOffset.x <= maxRight { // not right bounce self.currentPage = isRTL ? self.currentPage + 1 : self.currentPage - 1 } } else { if scrollView.contentOffset.x >= 0 { // not left bounce self.currentPage = isRTL ? self.currentPage - 1 : self.currentPage + 1 } } self.currentPage = max(0, self.currentPage) self.currentPage = min(self.numberOfPages - 1, self.currentPage) var offset = targetContentOffset.pointee if isRTL { offset.x = CGFloat(self.numberOfPages - self.currentPage - 1) * pageWidth } else { offset.x = CGFloat(self.currentPage) * pageWidth } targetContentOffset.pointee = offset } }
UICollectionView Cell Scroll to centre
I am using UICollectionView in my UIViewController. My collectionview properties are set as below. Now I would like cell to be Centre on screen after scroll! Option 1: Option 2: 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); }
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) } }
you can use code self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
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 } }
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
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() } }
I don't know why everybody's answer is so complicated, I simply turn on Paging enabled in Interface Builder and it works perfectly.
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 } }
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
Paging UICollectionView by cells, not screen
I have UICollectionView with horizontal scrolling and there are always 2 cells side-by-side per the entire screen. I need the scrolling to stop at the begining of a cell. With paging enabled, the collection view scrolls the whole page, which is 2 cells at once, and then it stops. I need to enable scrolling by a single cell, or scrolling by multiple cells with stopping at the edge of the cell. I tried to subclass UICollectionViewFlowLayout and to implement the method targetContentOffsetForProposedContentOffset, but so far I was only able to break my collection view and it stopped scrolling. Is there any easier way to achieve this and how, or do I really need to implement all methods of UICollectionViewFlowLayout subclass? Thanks.
OK, so I found the solution here: targetContentOffsetForProposedContentOffset:withScrollingVelocity without subclassing UICollectionViewFlowLayout I should have searched for targetContentOffsetForProposedContentOffset in the begining.
Here's my implementation in Swift 5 for vertical cell-based paging: override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { guard let collectionView = self.collectionView else { let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) return latestOffset } // Page height used for estimating and calculating paging. let pageHeight = self.itemSize.height + self.minimumLineSpacing // Make an estimation of the current page position. let approximatePage = collectionView.contentOffset.y/pageHeight // Determine the current page based on velocity. let currentPage = velocity.y == 0 ? round(approximatePage) : (velocity.y < 0.0 ? floor(approximatePage) : ceil(approximatePage)) // Create custom flickVelocity. let flickVelocity = velocity.y * 0.3 // Check how many pages the user flicked, if <= 1 then flickedPages should return 0. let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity) let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - collectionView.contentInset.top return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset) } Some notes: Doesn't glitch SET PAGING TO FALSE! (otherwise this won't work) Allows you to set your own flickvelocity easily. If something is still not working after trying this, check if your itemSize actually matches the size of the item as that's often a problem, especially when using collectionView(_:layout:sizeForItemAt:), use a custom variable with the itemSize instead. This works best when you set self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast. Here's a horizontal version (haven't tested it thoroughly so please forgive any mistakes): override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { guard let collectionView = self.collectionView else { let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) return latestOffset } // Page width used for estimating and calculating paging. let pageWidth = self.itemSize.width + self.minimumInteritemSpacing // Make an estimation of the current page position. let approximatePage = collectionView.contentOffset.x/pageWidth // Determine the current page based on velocity. let currentPage = velocity.x == 0 ? round(approximatePage) : (velocity.x < 0.0 ? floor(approximatePage) : ceil(approximatePage)) // Create custom flickVelocity. let flickVelocity = velocity.x * 0.3 // Check how many pages the user flicked, if <= 1 then flickedPages should return 0. let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity) // Calculate newHorizontalOffset. let newHorizontalOffset = ((currentPage + flickedPages) * pageWidth) - collectionView.contentInset.left return CGPoint(x: newHorizontalOffset, y: proposedContentOffset.y) } This code is based on the code I use in my personal project, you can check it out here by downloading it and running the Example target.
just override the method: - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { *targetContentOffset = scrollView.contentOffset; // set acceleration to 0.0 float pageWidth = (float)self.articlesCollectionView.bounds.size.width; int minSpace = 10; int cellToSwipe = (scrollView.contentOffset.x)/(pageWidth + minSpace) + 0.5; // cell width + min spacing for lines if (cellToSwipe < 0) { cellToSwipe = 0; } else if (cellToSwipe >= self.articles.count) { cellToSwipe = self.articles.count - 1; } [self.articlesCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:cellToSwipe inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES]; }
Horizontal Paging With Custom Page Width (Swift 4 & 5) Many solutions presented here result in some weird behaviour that doesn't feel like properly implemented paging. The solution presented in this tutorial, however, doesn't seem to have any issues. It just feels like a perfectly working paging algorithm. You can implement it in 5 simple steps: Add the following property to your type: private var indexOfCellBeforeDragging = 0 Set the collectionView delegate like this: collectionView.delegate = self Add conformance to UICollectionViewDelegate via an extension: extension YourType: UICollectionViewDelegate { } Add the following method to the extension implementing the UICollectionViewDelegate conformance and set a value for pageWidth: func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { let pageWidth = // The width your page should have (plus a possible margin) let proportionalOffset = collectionView.contentOffset.x / pageWidth indexOfCellBeforeDragging = Int(round(proportionalOffset)) } Add the following method to the extension implementing the UICollectionViewDelegate conformance, set the same value for pageWidth (you may also store this value at a central place) and set a value for collectionViewItemCount: func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { // Stop scrolling targetContentOffset.pointee = scrollView.contentOffset // Calculate conditions let pageWidth = // The width your page should have (plus a possible margin) let collectionViewItemCount = // The number of items in this section let proportionalOffset = collectionView.contentOffset.x / pageWidth let indexOfMajorCell = Int(round(proportionalOffset)) let swipeVelocityThreshold: CGFloat = 0.5 let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < collectionViewItemCount && velocity.x > swipeVelocityThreshold let hasEnoughVelocityToSlideToThePreviousCell = indexOfCellBeforeDragging - 1 >= 0 && velocity.x < -swipeVelocityThreshold let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell) if didUseSwipeToSkipCell { // Animate so that swipe is just continued let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1) let toValue = pageWidth * CGFloat(snapToIndex) UIView.animate( withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: .allowUserInteraction, animations: { scrollView.contentOffset = CGPoint(x: toValue, y: 0) scrollView.layoutIfNeeded() }, completion: nil ) } else { // Pop back (against velocity) let indexPath = IndexPath(row: indexOfMajorCell, section: 0) collectionView.scrollToItem(at: indexPath, at: .left, animated: true) } }
Here's the easiest way that i found to do that in Swift 4.2 for horinzontal scroll: I'm using the first cell on visibleCells and scrolling to then, if the first visible cell are showing less of the half of it's width i'm scrolling to the next one. If your collection scroll vertically, simply change x by y and width by height func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { targetContentOffset.pointee = scrollView.contentOffset var indexes = self.collectionView.indexPathsForVisibleItems indexes.sort() var index = indexes.first! let cell = self.collectionView.cellForItem(at: index)! let position = self.collectionView.contentOffset.x - cell.frame.origin.x if position > cell.frame.size.width/2{ index.row = index.row+1 } self.collectionView.scrollToItem(at: index, at: .left, animated: true ) }
Swift 3 version of Evya's answer: func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { targetContentOffset.pointee = scrollView.contentOffset let pageWidth:Float = Float(self.view.bounds.width) let minSpace:Float = 10.0 var cellToSwipe:Double = Double(Float((scrollView.contentOffset.x))/Float((pageWidth+minSpace))) + Double(0.5) if cellToSwipe < 0 { cellToSwipe = 0 } else if cellToSwipe >= Double(self.articles.count) { cellToSwipe = Double(self.articles.count) - Double(1) } let indexPath:IndexPath = IndexPath(row: Int(cellToSwipe), section:0) self.collectionView.scrollToItem(at:indexPath, at: UICollectionViewScrollPosition.left, animated: true) }
Partly based on StevenOjo's answer. I've tested this using a horizontal scrolling and no Bounce UICollectionView. cellSize is CollectionViewCell size. You can tweak factor to modify scrolling sensitivity. override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { targetContentOffset.pointee = scrollView.contentOffset var factor: CGFloat = 0.5 if velocity.x < 0 { factor = -factor } let indexPath = IndexPath(row: (scrollView.contentOffset.x/cellSize.width + factor).int, section: 0) collectionView?.scrollToItem(at: indexPath, at: .left, animated: true) }
Approach 1: Collection View flowLayout is UICollectionViewFlowLayout property override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { if let collectionView = collectionView { targetContentOffset.memory = scrollView.contentOffset let pageWidth = CGRectGetWidth(scrollView.frame) + flowLayout.minimumInteritemSpacing var assistanceOffset : CGFloat = pageWidth / 3.0 if velocity.x < 0 { assistanceOffset = -assistanceOffset } let assistedScrollPosition = (scrollView.contentOffset.x + assistanceOffset) / pageWidth var targetIndex = Int(round(assistedScrollPosition)) if targetIndex < 0 { targetIndex = 0 } else if targetIndex >= collectionView.numberOfItemsInSection(0) { targetIndex = collectionView.numberOfItemsInSection(0) - 1 } print("targetIndex = \(targetIndex)") let indexPath = NSIndexPath(forItem: targetIndex, inSection: 0) collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Left, animated: true) } } Approach 2: Page View Controller You could use UIPageViewController if it meets your requirements, each page would have a separate view controller.
Here is the optimised solution in Swift5, including handling the wrong indexPath. - Michael Lin Liu Step1. Get the indexPath of the current cell. Step2. Detect the velocity when scroll. Step3. Increase the indexPath's row when the velocity is increased. Step4. Tell the collection view to scroll to the next item func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { targetContentOffset.pointee = scrollView.contentOffset //M: Get the first visiable item's indexPath from visibaleItems. var indexPaths = *YOURCOLLECTIONVIEW*.indexPathsForVisibleItems indexPaths.sort() var indexPath = indexPaths.first! //M: Use the velocity to detect the paging control movement. //M: If the movement is forward, then increase the indexPath. if velocity.x > 0{ indexPath.row += 1 //M: If the movement is in the next section, which means the indexPath's row is out range. We set the indexPath to the first row of the next section. if indexPath.row == *YOURCOLLECTIONVIEW*.numberOfItems(inSection: indexPath.section){ indexPath.row = 0 indexPath.section += 1 } } else{ //M: If the movement is backward, the indexPath will be automatically changed to the first visiable item which is indexPath.row - 1. So there is no need to write the logic. } //M: Tell the collection view to scroll to the next item. *YOURCOLLECTIONVIEW*.scrollToItem(at: indexPath, at: .left, animated: true ) }
modify Romulo BM answer for velocity listening func scrollViewWillEndDragging( _ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint> ) { targetContentOffset.pointee = scrollView.contentOffset var indexes = collection.indexPathsForVisibleItems indexes.sort() var index = indexes.first! if velocity.x > 0 { index.row += 1 } else if velocity.x == 0 { let cell = self.collection.cellForItem(at: index)! let position = self.collection.contentOffset.x - cell.frame.origin.x if position > cell.frame.size.width / 2 { index.row += 1 } } self.collection.scrollToItem(at: index, at: .centeredHorizontally, animated: true ) }
This is a straight way to do this. The case is simple, but finally quite common ( typical thumbnails scroller with fixed cell size and fixed gap between cells ) var itemCellSize: CGSize = <your cell size> var itemCellsGap: CGFloat = <gap in between> override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { let pageWidth = (itemCellSize.width + itemCellsGap) let itemIndex = (targetContentOffset.pointee.x) / pageWidth targetContentOffset.pointee.x = round(itemIndex) * pageWidth - (itemCellsGap / 2) } // CollectionViewFlowLayoutDelegate func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return itemCellSize } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return itemCellsGap } Note that there is no reason to call a scrollToOffset or dive into layouts. The native scrolling behaviour already does everything. Cheers All :)
Kind of like evya's answer, but a little smoother because it doesn't set the targetContentOffset to zero. - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { if ([scrollView isKindOfClass:[UICollectionView class]]) { UICollectionView* collectionView = (UICollectionView*)scrollView; if ([collectionView.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]]) { UICollectionViewFlowLayout* layout = (UICollectionViewFlowLayout*)collectionView.collectionViewLayout; CGFloat pageWidth = layout.itemSize.width + layout.minimumInteritemSpacing; CGFloat usualSideOverhang = (scrollView.bounds.size.width - pageWidth)/2.0; // k*pageWidth - usualSideOverhang = contentOffset for page at index k if k >= 1, 0 if k = 0 // -> (contentOffset + usualSideOverhang)/pageWidth = k at page stops NSInteger targetPage = 0; CGFloat currentOffsetInPages = (scrollView.contentOffset.x + usualSideOverhang)/pageWidth; targetPage = velocity.x < 0 ? floor(currentOffsetInPages) : ceil(currentOffsetInPages); targetPage = MAX(0,MIN(self.projects.count - 1,targetPage)); *targetContentOffset = CGPointMake(MAX(targetPage*pageWidth - usualSideOverhang,0), 0); } } }
Here is my version of it in Swift 3. Calculate the offset after scrolling ended and adjust the offset with animation. collectionLayout is a UICollectionViewFlowLayout() func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { let index = scrollView.contentOffset.x / collectionLayout.itemSize.width let fracPart = index.truncatingRemainder(dividingBy: 1) let item= Int(fracPart >= 0.5 ? ceil(index) : floor(index)) let indexPath = IndexPath(item: item, section: 0) collectionView.scrollToItem(at: indexPath, at: .left, animated: true) }
Swift 5 I've found a way to do this without subclassing UICollectionView, just calculating the contentOffset in horizontal. Obviously without isPagingEnabled set true. Here is the code: var offsetScroll1 : CGFloat = 0 var offsetScroll2 : CGFloat = 0 let flowLayout = UICollectionViewFlowLayout() let screenSize : CGSize = UIScreen.main.bounds.size var items = ["1", "2", "3", "4", "5"] override func viewDidLoad() { super.viewDidLoad() flowLayout.scrollDirection = .horizontal flowLayout.minimumLineSpacing = 7 let collectionView = UICollectionView(frame: CGRect(x: 0, y: 590, width: screenSize.width, height: 200), collectionViewLayout: flowLayout) collectionView.register(collectionViewCell1.self, forCellWithReuseIdentifier: cellReuseIdentifier) collectionView.delegate = self collectionView.dataSource = self collectionView.backgroundColor = UIColor.clear collectionView.showsHorizontalScrollIndicator = false self.view.addSubview(collectionView) } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { offsetScroll1 = offsetScroll2 } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { offsetScroll1 = offsetScroll2 } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>){ let indexOfMajorCell = self.desiredIndex() let indexPath = IndexPath(row: indexOfMajorCell, section: 0) flowLayout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) targetContentOffset.pointee = scrollView.contentOffset } private func desiredIndex() -> Int { var integerIndex = 0 print(flowLayout.collectionView!.contentOffset.x) offsetScroll2 = flowLayout.collectionView!.contentOffset.x if offsetScroll2 > offsetScroll1 { integerIndex += 1 let offset = flowLayout.collectionView!.contentOffset.x / screenSize.width integerIndex = Int(round(offset)) if integerIndex < (items.count - 1) { integerIndex += 1 } } if offsetScroll2 < offsetScroll1 { let offset = flowLayout.collectionView!.contentOffset.x / screenSize.width integerIndex = Int(offset.rounded(.towardZero)) } let targetIndex = integerIndex return targetIndex }
Also you can create fake scroll view to handle scrolling. Horizontal or Vertical // === Defaults === let bannerSize = CGSize(width: 280, height: 170) let pageWidth: CGFloat = 290 // ^ + paging let insetLeft: CGFloat = 20 let insetRight: CGFloat = 20 // ================ var pageScrollView: UIScrollView! override func viewDidLoad() { super.viewDidLoad() // Create fake scrollview to properly handle paging pageScrollView = UIScrollView(frame: CGRect(origin: .zero, size: CGSize(width: pageWidth, height: 100))) pageScrollView.isPagingEnabled = true pageScrollView.alwaysBounceHorizontal = true pageScrollView.showsVerticalScrollIndicator = false pageScrollView.showsHorizontalScrollIndicator = false pageScrollView.delegate = self pageScrollView.isHidden = true view.insertSubview(pageScrollView, belowSubview: collectionView) // Set desired gesture recognizers to the collection view for gr in pageScrollView.gestureRecognizers! { collectionView.addGestureRecognizer(gr) } } func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView == pageScrollView { // Return scrolling back to the collection view collectionView.contentOffset.x = pageScrollView.contentOffset.x } } func refreshData() { ... refreshScroll() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() refreshScroll() } /// Refresh fake scrolling view content size if content changes func refreshScroll() { let w = collectionView.width - bannerSize.width - insetLeft - insetRight pageScrollView.contentSize = CGSize(width: pageWidth * CGFloat(banners.count) - w, height: 100) }
This is my solution, in Swift 4.2, I wish it could help you. class SomeViewController: UIViewController { private lazy var flowLayout: UICollectionViewFlowLayout = { let layout = UICollectionViewFlowLayout() layout.itemSize = CGSize(width: /* width */, height: /* height */) layout.minimumLineSpacing = // margin layout.minimumInteritemSpacing = 0.0 layout.sectionInset = UIEdgeInsets(top: 0.0, left: /* margin */, bottom: 0.0, right: /* margin */) layout.scrollDirection = .horizontal return layout }() private lazy var collectionView: UICollectionView = { let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) collectionView.showsHorizontalScrollIndicator = false collectionView.dataSource = self collectionView.delegate = self // collectionView.register(SomeCell.self) return collectionView }() private var currentIndex: Int = 0 } // MARK: - UIScrollViewDelegate extension SomeViewController { func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { guard scrollView == collectionView else { return } let pageWidth = flowLayout.itemSize.width + flowLayout.minimumLineSpacing currentIndex = Int(scrollView.contentOffset.x / pageWidth) } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { guard scrollView == collectionView else { return } let pageWidth = flowLayout.itemSize.width + flowLayout.minimumLineSpacing var targetIndex = Int(roundf(Float(targetContentOffset.pointee.x / pageWidth))) if targetIndex > currentIndex { targetIndex = currentIndex + 1 } else if targetIndex < currentIndex { targetIndex = currentIndex - 1 } let count = collectionView.numberOfItems(inSection: 0) targetIndex = max(min(targetIndex, count - 1), 0) print("targetIndex: \(targetIndex)") targetContentOffset.pointee = scrollView.contentOffset var offsetX: CGFloat = 0.0 if targetIndex < count - 1 { offsetX = pageWidth * CGFloat(targetIndex) } else { offsetX = scrollView.contentSize.width - scrollView.width } collectionView.setContentOffset(CGPoint(x: offsetX, y: 0.0), animated: true) } }
The original answer of Олень Безрогий had an issue, so on the last cell collection view was scrolling to the beginning func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { targetContentOffset.pointee = scrollView.contentOffset var indexes = yourCollectionView.indexPathsForVisibleItems indexes.sort() var index = indexes.first! // if velocity.x > 0 && (Get the number of items from your data) > index.row + 1 { if velocity.x > 0 && yourCollectionView.numberOfItems(inSection: 0) > index.row + 1 { index.row += 1 } else if velocity.x == 0 { let cell = yourCollectionView.cellForItem(at: index)! let position = yourCollectionView.contentOffset.x - cell.frame.origin.x if position > cell.frame.size.width / 2 { index.row += 1 } } yourCollectionView.scrollToItem(at: index, at: .centeredHorizontally, animated: true ) }
Ok so the proposed answers did'nt worked for me because I wanted to scroll by sections instead, and thus, have variable width page sizes I did this (vertical only): var pagesSizes = [CGSize]() func scrollViewDidScroll(_ scrollView: UIScrollView) { defer { lastOffsetY = scrollView.contentOffset.y } if collectionView.isDecelerating { var currentPage = 0 var currentPageBottom = CGFloat(0) for pagesSize in pagesSizes { currentPageBottom += pagesSize.height if currentPageBottom > collectionView!.contentOffset.y { break } currentPage += 1 } if collectionView.contentOffset.y > currentPageBottom - pagesSizes[currentPage].height, collectionView.contentOffset.y + collectionView.frame.height < currentPageBottom { return // 100% of view within bounds } if lastOffsetY < collectionView.contentOffset.y { if currentPage + 1 != pagesSizes.count { collectionView.setContentOffset(CGPoint(x: 0, y: currentPageBottom), animated: true) } } else { collectionView.setContentOffset(CGPoint(x: 0, y: currentPageBottom - pagesSizes[currentPage].height), animated: true) } } } In this case, I calculate each page size beforehand using the section height + header + footer, and store it in the array. That's the pagesSizes member
i created a custom collection view layout here that supports: paging one cell at a time paging 2+ cells at a time depending on swipe velocity horizontal or vertical directions it's as easy as: let layout = PagingCollectionViewLayout() layout.itemSize = layout.minimumLineSpacing = layout.scrollDirection = you can just add PagingCollectionViewLayout.swift to your project or add pod 'PagingCollectionViewLayout' to your podfile
final class PagingFlowLayout: UICollectionViewFlowLayout { private var currentIndex = 0 override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { let count = collectionView!.numberOfItems(inSection: 0) let currentAttribute = layoutAttributesForItem( at: IndexPath(item: currentIndex, section: 0) ) ?? UICollectionViewLayoutAttributes() let direction = proposedContentOffset.x > currentAttribute.frame.minX if collectionView!.contentOffset.x + collectionView!.bounds.width < collectionView!.contentSize.width || currentIndex < count - 1 { currentIndex += direction ? 1 : -1 currentIndex = max(min(currentIndex, count - 1), 0) } let indexPath = IndexPath(item: currentIndex, section: 0) let closestAttribute = layoutAttributesForItem(at: indexPath) ?? UICollectionViewLayoutAttributes() let centerOffset = collectionView!.bounds.size.width / 2 return CGPoint(x: closestAttribute.center.x - centerOffset, y: 0) } }
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { targetContentOffset.pointee = scrollView.contentOffset var indexes = self.collectionHome.indexPathsForVisibleItems indexes.sort() var index = indexes.first! let cell = self.collectionHome.cellForItem(at: index)! let position = self.collectionHome.contentOffset.x - cell.frame.origin.x if position > cell.frame.size.width/2{ index.row = index.row+1 } self.collectionHome.scrollToItem(at: index, at: .left, animated: true ) }
It is the best solution I ever seen. Just use it with .linear type. https://github.com/nicklockwood/iCarousel God bless the author!:)
Here is my way to do it by using a UICollectionViewFlowLayout to override the targetContentOffset: (Although in the end, I end up not using this and use UIPageViewController instead.) /** A UICollectionViewFlowLayout with... - paged horizontal scrolling - itemSize is the same as the collectionView bounds.size */ class PagedFlowLayout: UICollectionViewFlowLayout { override init() { super.init() self.scrollDirection = .horizontal self.minimumLineSpacing = 8 // line spacing is the horizontal spacing in horizontal scrollDirection self.minimumInteritemSpacing = 0 if #available(iOS 11.0, *) { self.sectionInsetReference = .fromSafeArea // for iPhone X } } required init?(coder aDecoder: NSCoder) { fatalError("not implemented") } // Note: Setting `minimumInteritemSpacing` here will be too late. Don't do it here. override func prepare() { super.prepare() guard let collectionView = collectionView else { return } collectionView.decelerationRate = UIScrollViewDecelerationRateFast // mostly you want it fast! let insetedBounds = UIEdgeInsetsInsetRect(collectionView.bounds, self.sectionInset) self.itemSize = insetedBounds.size } // Table: Possible cases of targetContentOffset calculation // ------------------------- // start | | // near | velocity | end // page | | page // ------------------------- // 0 | forward | 1 // 0 | still | 0 // 0 | backward | 0 // 1 | forward | 1 // 1 | still | 1 // 1 | backward | 0 // ------------------------- override func targetContentOffset( //swiftlint:disable:this cyclomatic_complexity forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { guard let collectionView = collectionView else { return proposedContentOffset } let pageWidth = itemSize.width + minimumLineSpacing let currentPage: CGFloat = collectionView.contentOffset.x / pageWidth let nearestPage: CGFloat = round(currentPage) let isNearPreviousPage = nearestPage < currentPage var pageDiff: CGFloat = 0 let velocityThreshold: CGFloat = 0.5 // can customize this threshold if isNearPreviousPage { if velocity.x > velocityThreshold { pageDiff = 1 } } else { if velocity.x < -velocityThreshold { pageDiff = -1 } } let x = (nearestPage + pageDiff) * pageWidth let cappedX = max(0, x) // cap to avoid targeting beyond content //print("x:", x, "velocity:", velocity) return CGPoint(x: cappedX, y: proposedContentOffset.y) } }
You can use the following library: https://github.com/ink-spot/UPCarouselFlowLayout It's very simple and ofc you do not need to think about details like other answers contain.