UIScrollView custom paging size
Solution 1
There is a UIScrollView
delegate method you can use. Set your class as the scroll view's delegate, and then implement the following:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
CGFloat kMaxIndex = 23;
CGFloat targetX = scrollView.contentOffset.x + velocity.x * 60.0;
CGFloat targetIndex = 0.0;
if (velocity.x > 0) {
targetIndex = ceil(targetX / (kCellWidth + kCellSpacing));
} else if (velocity.x == 0) {
targetIndex = round(targetX / (kCellWidth + kCellSpacing));
} else if (velocity.x < 0) {
targetIndex = floor(targetX / (kCellWidth + kCellSpacing));
}
if (targetIndex < 0)
targetIndex = 0;
if (targetIndex > kMaxIndex)
targetIndex = kMaxIndex;
targetContentOffset->x = targetIndex * (kCellWidth + kCellSpacing);
//scrollView.decelerationRate = UIScrollViewDecelerationRateFast;//uncomment this for faster paging
}
The velocity parameter is necessary to make sure the scrolling feels natural and doesn't end abruptly when a touch ends with your finger still moving. The cell width and cell spacing are the page width and spacing between pages in your view. In this case, I'm using a UICollectionView
.
Solution 2
- Change your scrollView size to the page size you want
- Set your
scroll.clipsToBounds = NO
-
Create a UIView subclass (e.g HackClipView) and override the hitTest:withEvent: method
-(UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *)event { UIView* child = [super hitTest:point withEvent:event]; if (child == self && self.subviews.count > 0) { return self.subviews[0]; } return child; }
Set the
HackClipView.clipsToBounds = YES
- Put your scrollView in this HackClipView (with the total scrolling size you want)
See this answer for more details
Update:
As stated in lucius answer you can now implement the UIScollViewDelegate
protocol and use the - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
method. As the targetContentOffset
is a pointer. Using this method will not guarantee you the same result with scroll view pages as the user can scroll through many pages at once. But setting the descelerationRate
to fast
will almost give you the same result
Solution 3
You should disable paging and add a UIPanGestureRecognizer to your scroll view and handle the paging yourself.
- (void)viewDidLoad {
[super viewDidLoad];
CGRect viewRect = self.view.bounds; // View controller's view bounds
theScrollView = [[UIScrollView alloc] initWithFrame:viewRect];
theScrollView.scrollsToTop = NO;
theScrollView.pagingEnabled = NO;
theScrollView.delaysContentTouches = NO;
theScrollView.delegate = self;
[self.view addSubview:theScrollView];
UIPanGestureRecognizer * peter = [[[UIPanGestureRecognizer alloc] initWithTarget:self
action:@selector(handlePan:)]
autorelease];
[theScrollView addGestureRecognizer:peter];
}
-(void)handlePan:(UIPanGestureRecognizer*)recognizer{
switch (recognizer.state) {
case UIGestureRecognizerStateBegan:{
// panStart and startPoint are instance vars for the viewContainer
panStart = theScrollView.contentOffset;
startPoint = [recognizer locationInView:theScrollView];
break;
}
case UIGestureRecognizerStateChanged:{
CGPoint newPoint = [recognizer locationInView:theScrollView];
CGFloat delta = startPoint.x - newPoint.x;
if ( abs(delta) > 2)
theScrollView.contentOffset = CGPointMake( theScrollView.contentOffset.x + delta, 0);
CGFloat moveDelta = panStart.x - theScrollView.contentOffset.x;
// current witdh should hold the currently displayed page/view in theScrollView
if ( abs(moveDelta) > (currentWidth * 0.40)){
panStart = theScrollView.contentOffset;
startPoint = newPoint;
//NSLog(@"delta is bigger");
if ( moveDelta < 0 )
[self incrementPageNumber]; // you should implement this method and present the next view
else
[self decrementPageNumber]; // you should implement this method and present the previous view
recognizer.enabled = NO; // disable further event until view change finish
}
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
recognizer.enabled = YES;
[self showDocumentPage:currentPage];
break;
default:
break;
}
}
Solution 4
Swift 4.1 solution that simplifies reusing:
/// Protocol that simplifies custom page size configuration for UIScrollView.
/// Sadly, can not be done better due to protocol extensions limitations - https://stackoverflow.com/questions/39487168/non-objc-method-does-not-satisfy-optional-requirement-of-objc-protocol
/// - note: Set `.decelerationRate` to `UIScrollViewDecelerationRateFast` for a fancy scrolling animation.
protocol ScrollViewCustomHorizontalPageSize: UIScrollViewDelegate {
/// Custom page size
var pageSize: CGFloat { get }
/// Helper method to get current page fraction
func getCurrentPage(scrollView: UIScrollView) -> CGFloat
/// Helper method to get targetContentOffset. Usage:
///
/// func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
/// targetContentOffset.pointee.x = getTargetContentOffset(scrollView: scrollView, velocity: velocity)
/// }
func getTargetContentOffset(scrollView: UIScrollView, velocity: CGPoint) -> CGFloat
/// Must be implemented. See `getTargetContentOffset` for more info.
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
}
extension ScrollViewCustomHorizontalPageSize {
func getCurrentPage(scrollView: UIScrollView) -> CGFloat {
return (scrollView.contentOffset.x + scrollView.contentInset.left) / pageSize
}
func getTargetContentOffset(scrollView: UIScrollView, velocity: CGPoint) -> CGFloat {
let targetX: CGFloat = scrollView.contentOffset.x + velocity.x * 60.0
var targetIndex = (targetX + scrollView.contentInset.left) / pageSize
let maxOffsetX = scrollView.contentSize.width - scrollView.bounds.width + scrollView.contentInset.right
let maxIndex = (maxOffsetX + scrollView.contentInset.left) / pageSize
if velocity.x > 0 {
targetIndex = ceil(targetIndex)
} else if velocity.x < 0 {
targetIndex = floor(targetIndex)
} else {
let (maxFloorIndex, lastInterval) = modf(maxIndex)
if targetIndex > maxFloorIndex {
if targetIndex >= lastInterval / 2 + maxFloorIndex {
targetIndex = maxIndex
} else {
targetIndex = maxFloorIndex
}
} else {
targetIndex = round(targetIndex)
}
}
if targetIndex < 0 {
targetIndex = 0
}
var offsetX = targetIndex * pageSize - scrollView.contentInset.left
offsetX = min(offsetX, maxOffsetX)
return offsetX
}
}
Just conform to ScrollViewCustomPageSize
protocol in your UIScrollView/UITableView/UICollectionView delegate and you are done, e.g.:
extension MyCollectionViewController: ScrollViewCustomPageSize {
var pageSize: CGFloat {
return 200
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee.x = getTargetContentOffset(scrollView: scrollView, velocity: velocity)
}
}
For a fancy scrolling I also recommend to set collectionView.decelerationRate = UIScrollViewDecelerationRateFast
Solution 5
I had the same problem so I have made a custom UIScrollView. It's available on Github now because when I searched I didn't find any solutions like this. Enjoy! https://github.com/MartinMetselaar/MMCPSScrollView
MMCPSScrollView* scrollView = [[MMCPSScrollView alloc] initWithFrame:self.view.bounds];
[scrollView setType:MMCPSScrollVertical];
[scrollView setPageHeight:250];
[scrollView setPageSize:2];
[self.view addSubview:scrollView];
If you have any further questions about this component, just ask.
Related videos on Youtube
user784625
Updated on July 09, 2022Comments
-
user784625 almost 2 years
paging in UIScrollView is a great feature, what I need here is to set the paging to a smaller distance, for example I want my UIScrollView to page less size that the UIScrollView frame width. Thanks
-
Warif Akhand Rishi over 8 yearssee this answer stackoverflow.com/a/35197919/1378447
-
Angel G. Olloqui about 8 yearsI just shared a much more simple way of doing it here: stackoverflow.com/a/36641652/378433
-
-
user784625 almost 13 yearsit does not help me, I want something animated
-
user784625 almost 13 yearsit does not help me, I'm already have 2 UIScrollViews neasted
-
Joshua almost 11 yearsThis is a bad answer. If you make the scrollview smaller then the touchable area is also shrunk to the frame you set. The question specifically asks how to handle "a page less than the UIScrollView frame width".
-
Francescu almost 11 years@Joshua, no, by overriding the hitTest method on the superview you can redirect the touch on the scrollView child.
-
thgc over 10 yearsI upvoted because this is a different and functioning solution and provides more customizability to the scrolling behavior.
-
Frank Schmitt almost 10 years@Francescu @Joshua or override
pointInside:withEvent:
in a scroll view subclass. -
nikans almost 10 yearsTerribly bad idea in most cases
-
Kevin Hirsch almost 10 yearsIf you want to avoid the scrollview jumping to its initial position when making very small swipes, you may use this :
if (velocity.x > 0) { targetIndex = ceil(targetX / (self.cardViewWidth + kCardSpacing)); } else { targetIndex = floor(targetX / (self.cardViewWidth + kCardSpacing)); }
-
pash3r over 9 yearshi, @lucius! Your code seems to work but I have 1 problem! My pageSize = 340. TargetContentOffset.x calculates correctly, but my scrollView stops at the wrong position so I see margin between views and view (or cell, doesn't matter) (but it has to stop in right position). Maybe u can tell something about it? :) Thanks in advance!
-
pash3r over 9 years@lucious, I've figured it out: I had to set scrollView.pagingEnabled to NO :)
-
KoCMoHaBTa over 9 yearsWhy targetX expression is multiplied by 60 at the end? This is the only part that i cannot understand. From where does this magic number comes?
-
Martin over 8 yearsWon't work with UITableView & UICollectionView, cause the cells out of the original bounds (normally "hidden" but visible thanks to
clipToBounds=NO
) will be removed from superview. -
Морт over 7 years@KoCMoHaBTa "velocity is in points/millisecond" as per the documentation, so this takes into account the force of the drag, by adding the distance that the scroll view would travel in the next 60ms. More powerful drags will scroll further.
-
MLQ over 7 years@lucius But how do we know that the deceleration animation will run for exactly 60ms?
-
Robert over 6 yearsFrom trial and error I found that a constant of 900 instead of 60 avoids a glitch when you scroll a small distance very fast.
-
Max Chuquimia over 6 yearsDon't forget to have a play with
scrollView.decelerationRate
to get the feel you are looking for - I had to set it toUIScrollViewDecelerationRateFast
before it felt natural -
Fantini about 6 yearsI think that you should elaborate a bit more. Why does your code works?
-
codrut almost 6 yearsTried with a UICollectionView and the cells from outside the visible area are hidden/reused, except the next one in the direction of scrolling. Did not succeed to trick it to cache more cells. Also, drag touch works only if it starts inside the small scrollView
-
mkz about 5 yearsis there a way to lock scroll to only one page per action? using following approach I sometimes can scroll though two pages, but want only one in each direction. sometimes now: 0 - 1 - 3 - 4, want : 0 - 1 - 2 - 3 - 4
-
Anton Plebanovich about 5 years@mkz hi, you can just clamp
targetIndex
before calculatingoffsetX
ingetTargetContentOffset
method. I didn't test but something like that should work:targetIndex = max(targetIndex, floor(getCurrentPage(scrollView: scrollView)); targetIndex = min(targetIndex, ceil(getCurrentPage(scrollView: scrollView))
-
krishan kumar about 5 yearsIt will be more helpful, if you give swift implementation also. Because i am not able to assign value to "targetContentOffset"
-
Albert Renshaw almost 5 yearsIt seems the suggestion by @KevinHirsch is not just a preference but an actual fix to a bug in the behavior of the script by Lucious, I'm therefor editing it in to the answer. While I'm at it I added optional (commented out) high value snippets from the comments.
-
Albert Renshaw almost 5 yearsAdditionally, Kevin's solution has a small issue in which if you page to the right, then over half way through the paging animation, if you 'tap' the scrollview w/o swiping at all, it will launch you back to the previous page. I've fixed this by adding a conditional to use
round
if no velocity.x was found (i.e. tap not swipe) -
Alen Liang over 4 years@MaxChuquimia Your comment saved my day!
-
bruno over 3 yearsThe methods are triggered but there's no limit in the width. Do you have an example?
-
Anton Plebanovich over 3 years@bruno you may check github.com/APUtils/APExtensions/tree/… there is a custom page size example
-
stefanosn about 2 years@AlbertRenshaw What if the cell has dynamic height? How do i get the height of each UITableViewcell from my UItableview in scrollViewWillEndDragging so i can use a dynamic kCellHeight? I need this because not all cells have the same height in my tableview...any help appreciated.