Enabling paging in a UIScrollView will allow you to have a nice ‘snap-to’ effect. It’s exactly what you see when using the Photo app. I recently wanted to utilize this effect while displaying multiple pages on the screen but ran into a few issues. Problems and solutions below. Also, you can picture any ‘pages’ that I make reference to will just display an image as content.
Problem 1 – Swipe Not Detected
Consider this: You have a page in the center of the screen. To the left you see half of another page and to the right you see half of another page. Setting this up should be easy. Uncheck ‘Clip Subviews’, check ‘Paging Enabled’ and set the frame to be the size of each page. Great, it all works! Except you can only swipe if touch begins in the center page. So if I start my swiping in one of the pages that is partly showing on the right or left then nothing would happen. While some would accept this as a mild annoyance and move on, not the perfectionists professionals at Accella!
Solution 1 – Swipe Detected
The solution to this problem involves placing the UIScrollView inside a UIView and overriding the hitTest method of the UIView to pass along the touch to the child UIScrollView. So start by subclassing the UIView, add a reference to the UIScrollView inside UIView and finally add the following code to your UIView subclass:
- (UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *)event { if ([self pointInside:point withEvent:event]) { return scrollView; } return nil; }Problem 2 – I want more than three pages on the screen
I actually needed to display 7 small images on the screen at one time. It was a horizontal bar that was the full width of the screen and held about 50 pages. Things were working in that I could swipe anywhere in that bar and I did get my nice ‘snap-to’ effect from the paging. However, things were a little too rigid. I got the ‘snap-to’ effect but had no momentum with my swipe. Swiping would instantly stop at the nearest page when I lifted my finger.
Solution 2 – I guess I don’t want paging after all
I determined that paging isn’t really what I was trying to achieve. What I really wanted was to have normal swiping, but have the UIScrollView ‘snap-to’ the center of the nearest page once it stopped. So it was a combination of ‘snap-to’ and momentum. To achieve this I entirely removed my previous solution and turned off paging. At this point I had just a free scrolling UIScrollView.
What I then did was monitored the UIScrollView to determine when it started to slow down. If it slowed down to a certain point then I knew it was about to stop. At that point we just set the content offset and set animation to ‘YES’. This provides a seamless animation for the whole process. I also need to have the ‘snap-to’ effect when just dragging, but not necessarily swiping.
#pragma mark - UIScrollViewDelegate // Note the width of my pages is set to 52px - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { monitorVelocity = decelerate; currentTime = [[NSDate date] timeIntervalSince1970]; prevTime = [[NSDate date] timeIntervalSince1970]; currentPosition = pickerScrollView.contentOffset.x; prevPosition = pickerScrollView.contentOffset.x; if(!decelerate) { CGFloat offset = round(currentPosition/52)*52; // only snap-to if offset is within content bounds to prevent interference with the scrollviews bounce property if(currentPosition > 0 && offset < pickerScrollView.contentSize.width && currentPosition < pickerScrollView.contentSize.width-52) { [pickerScrollView setContentOffset:CGPointMake(offset,0) animated:YES]; } } } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (monitorVelocity) { currentTime = [[NSDate date] timeIntervalSince1970]; currentPosition = pickerScrollView.contentOffset.x; float velocity = abs((currentPosition - prevPosition) / (currentTime - prevTime)); prevTime = currentTime; prevPosition = currentPosition; if (velocity 10) { monitorVelocity = NO; CGFloat offset = round(currentPosition/52)*52; // only snap-to if offset is within content bounds to prevent interference with the scrollviews bounce property if(currentPosition > 0 && offset < pickerScrollView.contentSize.width && currentPosition < pickerScrollView.contentSize.width-52) { [pickerScrollView setContentOffset:CGPointMake(offset,0) animated:YES]; } } } }
One Response
Thx, that got me started. One thing I would add is scrollViewDidEndDecelerating and a call to snap the position. It is possible for the velocity to drop from something like like 14 to 0, so the call to snap in scrollViewDidScroll might never be called. Alternatively, you could completely eliminate the if(velocity < 10) in scrollViewDidScroll completely and have it handled in scrollViewDidEndDecelerating.
Also, because setContentOffset:animated uses a constant velocity, the snap is more prominent than the animation used in the picker control which appears to adjust the speed of the snap based on the distance.
Cheers,
Ash