UIScrollView Custom Paging

My question has to do with a custom form of paging which I am trying to do with a scroller, and this is easier to visualise if you first consider the type of scroll view implemented in a slot machine.

So say my UIScrollView has a width of 100 pixels. Assume it contains 3 inner views, each with a width of 30 pixels, such that they are separated by a width of 3 pixels. The type of paging which I would like to achieve, is such that each page is one of my views (30 pixels), and not the whole width of the scroll view.

  • Placing and Moving Views on Rotation (UIView)
  • loadView: functions in UIView iOS
  • After rotation UIView coordinates are swapped but UIWindow's are not?
  • Auto-layout XIB embedded in programmatic UIView not sizing to parent
  • Loading custom UIView in UIViewController's main view
  • Drawing gradient on UIView not working with iOS 9
  • I know that usually, if the view takes up the whole width of the scroll view, and paging is enabled then everything works. However, in my custom paging, I also want surrounding views in the scroll view to be visible as well.

    How would I do this?

    6 Solutions Collect From Internet About “UIScrollView Custom Paging”

    I just did this for another project. What you need to do is to place the UIScrollView into a custom implementation of UIView. I created a class for this called ExtendedHitAreaViewController. The ExtendedHitAreaView overrides the hitTest function to return its first child object, which will be your scroll view.

    Your scroll view should be the page size you want, i.e., 30px with clipsToBounds = NO.
    The extended hit area view should be the full size of the area you want to be visible, with clipsToBounds = YES.

    Add the scroll view as a subview to the extended hit area view, then add the extended hit area view to your viewcontroller’s view.

    @implementation ExtendedHitAreaViewContainer
    
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        if ([self pointInside:point withEvent:event]) {
            if ([[self subviews] count] > 0) {
                //force return of first child, if exists
                return [[self subviews] objectAtIndex:0];
            } else {
                return self;
            }
        }
        return nil;
    }
    @end
    

    Since iOS 5 there is this delegate method: - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset.

    So you can do something like this:

    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint
    *)targetContentOffset {
        if (scrollView == self.scrollView) {
            CGFloat x = targetContentOffset->x;
            x = roundf(x / 30.0f) * 30.0f;
            targetContentOffset->x = x;
        } 
    }
    

    For higher velocities you might want to adjust your targetContentOffset a bit different if you want a more snappy feeling.

    I had the same problem and this worked great for me, tested on iOS 8.

    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
                     withVelocity:(CGPoint)velocity
              targetContentOffset:(inout CGPoint *)targetContentOffset
    {
        NSInteger index = lrint(targetContentOffset->x/_pageWidth);
        NSInteger currentPage = lrint(scrollView.contentOffset.x/_pageWidth);
    
        if(index == currentPage) {
            if(velocity.x > 0)
                index++;
            else if(velocity.x < 0)
                index--;
        }
    
        targetContentOffset->x = index * _pageWidth;
    }
    

    I had to check the velocity and always go to next/previous page if velocity was not zero, otherwise it would give non-animated jumps when doing very short and fast swipes.

    Update: This seems to work even better:

    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
                     withVelocity:(CGPoint)velocity
              targetContentOffset:(inout CGPoint *)targetContentOffset
    {
        CGFloat index = targetContentOffset->x/channelScrollWidth;
    
        if(velocity.x > 0)
            index = ceil(index);
        else if(velocity.x < 0)
            index = floor(index);
        else
            index = round(index);
    
        targetContentOffset->x = index * channelScrollWidth;
    }
    

    Those are for a horizontal scrollview, use y instead of x for a vertical one.

    After a couple of days of researching and troubleshooting i came up with something that works for me!

    First you need to subclass the view that the scrollview is in and override this method with the following:

        -(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
              UIView* child = nil;
              if ((child = [super hitTest:point withEvent:event]) == self)
                     return (UIView *)_calloutCell;
               return child;
        }
    

    Then all the magic happens in the scrollview delegate methods

        -(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
                //_lastOffset is declared in the header file
                //@property (nonatomic) CGPoint lastOffset;
                _lastOffset = scrollView.contentOffset;
         }
    
        - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    
                CGPoint currentOffset = scrollView.contentOffset;
                CGPoint newOffset = CGPointZero;
    
                if (_lastOffset.x < currentOffset.x) {
                    // right to left
                    newOffset.x = _lastOffset.x + 298;
                }
                else {
                     // left to right
                     newOffset.x = _lastOffset.x - 298;
                }
    
                [UIView beginAnimations:nil context:NULL];
                [UIView setAnimationDuration:0.5];
                [UIView setAnimationDelay:0.0f];
    
                targetContentOffset->x = newOffset.x;
                [UIView commitAnimations];
    
          }
    

    You also need to set the scrollview’s deceleration rate. I did it in ViewDidLoad

        [self.scrollView setDecelerationRate:UIScrollViewDecelerationRateFast];
    

    I have many different views inside scroll view with buttons and gesture recognisers.

    @picciano’s answer didn’t work (scroll worked good but buttons and recognisers didn’t get touches) for me so I found this solution:

    class ExtendedHitAreaView : UIScrollView {
        // Your insets
        var hitAreaEdgeInset = UIEdgeInsets(top: 0, left: -20, bottom: 0, right: -20) 
    
        override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
            let hitBounds = UIEdgeInsetsInsetRect(bounds, hitAreaEdgeInset)
            return CGRectContainsPoint(hitBounds, point)
        }
    }
    

    alcides’ solution works perfectly. i just enable / disable the scrolling of the scrollview, whenever i enter scrollviewDidEndDragging and scrollViewWillEndDragging. if the user scrolls several times before the paging animation is finished, the cells are slightly out of alignment.

    so i have:

    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    
        scrollView.scrollEnabled = NO
    
        CGPoint currentOffset = scrollView.contentOffset;
        CGPoint newOffset = CGPointZero;
    
        if (_lastOffset.x < currentOffset.x) {
            // right to left
            newOffset.x = _lastOffset.x + 298;
        }
        else {
            // left to right
            newOffset.x = _lastOffset.x - 298;
        }
    
        [UIView animateWithDuration:0.4 animations:^{
            targetContentOffset.x = newOffset.x
        }];
    }
    
    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView {
        scrollView.scrollEnabled = YES
    }