I managed to disable bouncing effect when user scrolls to the top. I would like to reverse it somehow so that the bouncing effect is disabled on the bottom. Could you help me with modifying my code for that purpose?
var lastY: CGFloat = 0.0
func scrollViewDidScroll(scrollView: UIScrollView) {
let currentY = scrollView.contentOffset.y
let currentBottomY = scrollView.frame.size.height + currentY
if currentY > lastY {
//"scrolling down"
tableView.bounces = true
} else {
//"scrolling up"
// Check that we are not in bottom bounce
if currentBottomY < scrollView.contentSize.height + scrollView.contentInset.bottom {
tableView.bounces = false
}
}
lastY = scrollView.contentOffset.y
}
Did you try setting bounce and VerticalBounce property to NO.
You can set these properties programatically like
In Swift
tableView.bounces = false
tableView.alwaysBounceVertical = false
In Objective-C
tableView.bounces = NO;
tableView.alwaysBounceVertical = NO;
OR
if you want to do it from Storyboard. Make you tableview setting like below :
You can try this code
override func scrollViewDidScroll(scrollView: UIScrollView)
{
if scrollView.contentOffset.y <= 0
{
scrollView.contentOffset = CGPointZero
}
}
Try this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == tableView {
if (self.tableView.contentOffset.y >= (self.tableView.contentSize.height - self.tableView.bounds.size.height))
{
scrollView.contentOffset = CGPoint(x: 0, y: (self.tableView.contentSize.height - self.tableView.bounds.size.height))
}
}
}
I am trying to implement a scroll view that snaps to points while scrolling.
All the posts here I've seen about snapping to a point 'after' the user has ended dragging the scroll. I want to make it snap during dragging.
So far I have this to stop the inertia after dragging and it works fine:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.memory = scrollView.contentOffset
}
I tried this but not working as desired:
var scrollSnapHeight : CGFloat = myScrollView.contentSize.height/10
scrollViewDidScroll:
func scrollViewDidScroll(scrollView: UIScrollView) {
let remainder : CGFloat = scrollView.contentOffset.y % scrollSnapHeight
var scrollPoint : CGPoint = scrollView.contentOffset
if remainder != 0 && scrollView.dragging
{
if self.lastOffset > scrollView.contentOffset.y //Scrolling Down
{
scrollPoint.y += (scrollSnapHeight - remainder)
NSLog("scrollDown")
}
else //Scrolling Up
{
scrollPoint.y -= (scrollSnapHeight - remainder)
}
scrollView .setContentOffset(scrollPoint, animated: true)
}
self.lastOffset = scrollView.contentOffset.y;
}
This approach is going to enable / disable scrollEnabled property of UIScrollView.
When scrollView scrolls outside the given scrollSnapHeight, make scrollEnabled to false. That will stop the scrolling. Then make scrolling enable again for the next drag.
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView.contentOffset.y > lastOffset + scrollSnapHeight {
scrollView.scrollEnabled = false
} else if scrollView.contentOffset.y < lastOffset - scrollSnapHeight {
scrollView.scrollEnabled = false
}
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
guard !decelerate else {
return
}
setContentOffset(scrollView)
}
func scrollViewWillBeginDecelerating(scrollView: UIScrollView) {
setContentOffset(scrollView)
}
}
func setContentOffset(scrollView: UIScrollView) {
let stopOver = scrollSnapHeight
var y = round(scrollView.contentOffset.y / stopOver) * stopOver
y = max(0, min(y, scrollView.contentSize.height - scrollView.frame.height))
lastOffset = y
scrollView.setContentOffset(CGPointMake(scrollView.contentOffset.x, y), animated: true)
scrollView.scrollEnabled = true
}
Subclass UIScrollView/UICollectionView
This solution does not require you lift your finger in order to unsnap and works while scrolling. If you need it for vertical scrolling and not horizontal scrolling just swap the x's with y's.
Set snapPoint to the content offset where you want the center of the snap to be.
Set snapOffset to the radius you want around the snapPoint for where snapping should occur.
If you need to know if the scrollView has snapped, just check the isSnapped variable.
class UIScrollViewSnapping : UIScrollView {
public var snapPoint: CGPoint?
public var snapOffset: CGFloat?
public var isSnapped = false
public override var contentOffset: CGPoint {
set {
if let snapPoint = self.snapPoint,
let snapOffset = self.snapOffset,
newValue.x > snapPoint.x - snapOffset,
newValue.x < snapPoint.x + snapOffset {
self.isSnapped = true
super.contentOffset = snapPoint
}
else {
self.isSnapped = false
super.contentOffset = newValue
}
}
get {
return super.contentOffset
}
}
}
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)
}
}
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
}
}
I'm trying to use the new scrollViewWillEndDragging:withVelocity:targetContentOffset: UIScrollView delegate call in iOS 5 but i can't seem to get it to actually respond to me correctly. I'm changing the targetContentOffset->x value but it never ends up being used. I know the code is being ran because it'll hit breakpoints in that function. I've even tried setting the offset value to a hard coded number so i'd know where it would end up but it never works.
Has anyone been able to use this correctly and make it work? Is there any other delegate call that must be implemented in order for this to work?
Here's my code in case someone sees something wrong with it:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
// goodOffsetX returns the contentOffset i want the scrollView to stop at
CGFloat goodOffsetX = [self _horizontalContentOffsetForTargetHorizontalContentOffset:(*targetContentOffset).x velocity:velocity.x];
NSLog( #" " );
NSLog( #"scrollViewWillEndDragging" );
NSLog( #" velocity: %f", velocity.x );
NSLog( #" currentX: %f", scrollView.contentOffset.x );
NSLog( #" uikit targetX: %f", (*targetContentOffset).x );
NSLog( #" pagedX: %f", goodOffsetX );
targetContentOffset->x = goodOffsetX;
}
You can implement custom paging with this code:
- (float) pageWidth {
return ((UICollectionViewFlowLayout*)self.collectionView.collectionViewLayout).itemSize.width +
((UICollectionViewFlowLayout*)self.collectionView.collectionViewLayout).minimumInteritemSpacing;
}
- (void) scrollViewWillBeginDragging:(UIScrollView *)scrollView {
CGFloat pageWidth = self.collectionView.frame.size.width + 10 /* Optional Photo app like gap between images. Or use [self pageWidth] in case if you want the next page be also visible */;
_currentPage = floor((self.collectionView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
NSLog(#"Dragging - You are now on page %i", _currentPage);
}
- (void) scrollViewWillEndDragging:(UIScrollView*)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint*)targetContentOffset {
CGFloat pageWidth = self.collectionView.frame.size.width + 10; // [self pageWidth]
int newPage = _currentPage;
if (velocity.x == 0) { // slow dragging not lifting finger
newPage = floor((targetContentOffset->x - pageWidth / 2) / pageWidth) + 1;
}
else {
newPage = velocity.x > 0 ? _currentPage + 1 : _currentPage - 1;
if (newPage < 0)
newPage = 0;
if (newPage > self.collectionView.contentSize.width / pageWidth)
newPage = ceil(self.collectionView.contentSize.width / pageWidth) - 1.0;
}
NSLog(#"Dragging - You will be on %i page (from page %i)", newPage, _currentPage);
*targetContentOffset = CGPointMake(newPage * pageWidth, targetContentOffset->y);
}
Of course you must set pagingEnabled = NO.
_currentPage is a class iVar.
Thanks to http://www.mysamplecode.com/2012/12/ios-scrollview-example-with-paging.html for pointing the right way.
SWIFT 3
With a demo here https://github.com/damienromito/CollectionViewCustom
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageWidth = Float(itemWidth + itemSpacing)
let targetXContentOffset = Float(targetContentOffset.pointee.x)
let contentWidth = Float(collectionView!.contentSize.width )
var newPage = Float(self.pageControl.currentPage)
if velocity.x == 0 {
newPage = floor( (targetXContentOffset - Float(pageWidth) / 2) / Float(pageWidth)) + 1.0
} else {
newPage = Float(velocity.x > 0 ? self.pageControl.currentPage + 1 : self.pageControl.currentPage - 1)
if newPage < 0 {
newPage = 0
}
if (newPage > contentWidth / pageWidth) {
newPage = ceil(contentWidth / pageWidth) - 1.0
}
}
let point = CGPoint (x: CGFloat(newPage * pageWidth), y: targetContentOffset.pointee.y)
targetContentOffset.pointee = point
}
I was able to run a quick test and got this to correctly fire and make my object stop as desired. I did this using the following simple test:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
targetContentOffset->x = scrollView.contentOffset.x - 10;
}
It seems that this method is likely not the issue in your code, but it is more likely that your 'goodOffsetX' is not correctly calculating a valid value to stop at.
Swift 2.2:
extension SomeCollectionViewController {
override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
let pageWidth = Float(collectionView!.frame.size.width)
let xCurrentOffset = Float(collectionView!.contentOffset.x)
currentPage = floor((xCurrentOffset - pageWidth / 2) / pageWidth) + 1
}
override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageWidth = Float(collectionView!.frame.size.width)
let targetXContentOffset = Float(targetContentOffset.memory.x)
let contentWidth = Float(collectionView!.contentSize.width)
var newPage = currentPage
if velocity.x == 0 {
newPage = floor((targetXContentOffset - pageWidth / 2) / pageWidth) + 1
} else {
newPage = velocity.x > 0 ? currentPage + 1 : currentPage - 1
if newPage < 0 {
newPage = 0
}
if newPage > contentWidth / pageWidth {
newPage = ceil(contentWidth / pageWidth) - 1.0
}
}
targetContentOffset.memory.x = CGFloat(newPage * pageWidth)
}
}
I also used the collectionView?.decelerationRate = UIScrollViewDecelerationRateFast as suggested by #skagedal to improve page speed.
public func scrollViewWillEndDragging(_ scrollView: UIScrollView ,withVelocity velocity: CGPoint, targetContentOffset:
UnsafeMutablePointer<CGPoint>){
let pageWidth = Float(appWidth + itemSpacing)
let targetXContentOffset = Float(targetContentOffset.pointee.x)
var newPage = Float(currentPageIndex)
// I use this way calculate newPage:
newPage = roundf(targetXContentOffset / pageWidth);
//if velocity.x == 0 {
// newPage = floor( (targetXContentOffset - Float(pageWidth) / 2) / Float(pageWidth)) + 1.0
//} else {
// newPage = Float(velocity.x > 0 ? newPage + 1 : newPage - 1)
// if newPage < 0 {
// newPage = 0
// }
// if (newPage > contentWidth / pageWidth) {
// newPage = ceil(contentWidth / pageWidth) - 1.0
// }
//}
let targetOffsetX = CGFloat(newPage * pageWidth)
let point = CGPoint (x: targetOffsetX, y: targetContentOffset.pointee.y)
targetContentOffset.pointee = point
}