Paging UIScrollView in increments smaller than frame size
There is also another solution wich is probably a little bit better than overlaying scroll view with another view and overriding hitTest.
You can subclass UIScrollView and override its pointInside. Then scroll view can respond for touches outside its frame. Of course the rest is the same.
@interface PagingScrollView : UIScrollView {
UIEdgeInsets responseInsets;
}
@property (nonatomic, assign) UIEdgeInsets responseInsets;
@end
@implementation PagingScrollView
@synthesize responseInsets;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint parentLocation = [self convertPoint:point toView:[self superview]];
CGRect responseRect = self.frame;
responseRect.origin.x -= responseInsets.left;
responseRect.origin.y -= responseInsets.top;
responseRect.size.width += (responseInsets.left + responseInsets.right);
responseRect.size.height += (responseInsets.top + responseInsets.bottom);
return CGRectContainsPoint(responseRect, parentLocation);
}
@end
The accepted answer is very good, but it will only work for the UIScrollView
class, and none of its descendants. For instance if you have lots of views and convert to a UICollectionView
, you will not be able to use this method, because the collection view will remove views that it thinks are "not visible" (so even though they aren't clipped, they will disappear).
The comment about that mentions scrollViewWillEndDragging:withVelocity:targetContentOffset:
is, in my opinion, the correct answer.
What you can do is, inside this delegate method you calculate the current page/index. Then you decide whether the velocity and target offset merit a "next page" movement. You can get pretty close to the pagingEnabled
behavior.
note: I'm usually a RubyMotion dev these days, so someone please proof this Obj-C code for correctness. Sorry for the mix of camelCase and snake_case, I copy&pasted much of this code.
- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetOffset
{
CGFloat x = targetOffset->x;
int index = [self convertXToIndex: x];
CGFloat w = 300f; // this is your custom page width
CGFloat current_x = w * [self convertXToIndex: scrollView.contentOffset.x];
// even if the velocity is low, if the offset is more than 50% past the halfway
// point, proceed to the next item.
if ( velocity.x < -0.5 || (current_x - x) > w / 2 ) {
index -= 1
}
else if ( velocity.x > 0.5 || (x - current_x) > w / 2 ) {
index += 1;
}
if ( index >= 0 || index < self.items.length ) {
CGFloat new_x = [self convertIndexToX: index];
targetOffset->x = new_x;
}
}
I see a lot of solutions, but they are very complex. A much easier way to have small pages but still keep all area scrollable, is to make the scroll smaller and move the scrollView.panGestureRecognizer
to your parent view. These are the steps:
Reduce your scrollView size
Make sure your scroll view is paginated and does not clip subview
In code, move the scrollview pan gesture to the parent container view that is full width:
override func viewDidLoad() {
super.viewDidLoad()
statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer)
}
Try making your scrollview less than the size of the screen (width-wise), but uncheck the "Clip Subviews" checkbox in IB. Then, overlay a transparent, userInteractionEnabled = NO view on top of it (at full width), which overrides hitTest:withEvent: to return your scroll view. That should give you what you're looking for. See this answer for more details.