Proper direction lock for an UIScrollView

Yesterday I stumbled upon an interesting problem. Say you have a UIScrollView with its property directionalLockEnabled = YES. You'd expect to only be able to scroll either horizontally or vertically at any point. Unfortunately the reality kicks in: yes, in theory you will scroll either horizontally or vertically, but there's a catch!


If you try to scroll diagonally you'll be able to move the UIScrollView in two directions at the same time - yes, it's not happening every time, but if you try hard enough it's not something really hidden. Let's see it in action.

As you can see, it's fairly easy to get the UIScrollView to scroll in both directions. So, the question is how can I make it to scroll just horizontally or vertically? I googled a bit and found an almost satisfying way on StackOverflow: Finding the direction of scrolling in a UIScrollView. This was giving the right direction (which was the initial question) but still made the UIScrollView go "crazy".

So this is what I did:

1. Define the ScrollDirection enum

typedef enum ScrollDirection {
    ScrollDirectionNone,
    ScrollDirectionCrazy,
    ScrollDirectionLeft,
    ScrollDirectionRight,
    ScrollDirectionUp,
    ScrollDirectionDown,
    ScrollDirectionHorizontal,
    ScrollDirectionVertical
} ScrollDirection;

2. Add a CGPoint to hold initial contentOffset of the UIScrollView

// scrollView initial offset
@property (nonatomic, assign) CGPoint initialContentOffset;

// also, this is my UIScrollView
@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;

3. Implementing UIScrollViewDelegate in the current ViewController

When the scrollView begins scrolling, I'm updating my initialContentOffset CGPoint to the scrollView.contentOffset

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    // For verbosity:
    //
    // _initialContentOffset.x = _scrollView.contentOffset.x;
    // _initialContentOffset.y = _scrollView.contentOffset.y;
    
    _initialContentOffset = _scrollView.contentOffset;
}

4. Create the methods that will retrieve the scrolling direction

I was mostly interested in getting the scrolling axis rather the direction itself so I wrote two methods: one for finding the direction and the other for finding the axis.

Comparing the initialContentOffset to the current scrollView.contentOffset enables us to determine the scroll direction.

- (ScrollDirection)determineScrollDirection: (UIScrollView *)scrollView
{
    ScrollDirection scrollDirection;
    
    // If the scrolling direction is changed on both X and Y it means the
    // scrolling started in one corner and goes diagonal. This will be
    // called ScrollDirectionCrazy
    
    if (_initialContentOffset.x != scrollView.contentOffset.x &&
        _initialContentOffset.y != scrollView.contentOffset.y) {
        scrollDirection = ScrollDirectionCrazy;
    } else {
        if (_initialContentOffset.x > scrollView.contentOffset.x) {
            scrollDirection = ScrollDirectionLeft;
        } else if (_initialContentOffset.x < scrollView.contentOffset.x) {
            scrollDirection = ScrollDirectionRight;
        } else if (_initialContentOffset.y > scrollView.contentOffset.y) {
            scrollDirection = ScrollDirectionUp;
        } else if (_initialContentOffset.y < scrollView.contentOffset.y) {
            scrollDirection = ScrollDirectionDown;
        } else {
            scrollDirection = ScrollDirectionNone;
        }
    }
    
    return scrollDirection;
}

- (ScrollDirection)determineScrollDirectionAxis: (UIScrollView *)scrollView
{
    ScrollDirection scrollDirection = [self determineScrollDirection: scrollView];
    
    switch (scrollDirection) {
        case ScrollDirectionLeft:
        case ScrollDirectionRight:
            return ScrollDirectionHorizontal;
            
        case ScrollDirectionUp:
        case ScrollDirectionDown:
            return ScrollDirectionVertical;
            
        default:
            return ScrollDirectionNone;
    }
}

5. Implementing scrollViewDidScroll:

This is the last step. I'm getting the ScrollDirection from the methods above and logging if it's

  • ScrollDirectionVertical or
  • ScrollDirectionHorizontal

If it's not one of them, I'm creating a new CGPoint and based on the current _scrollView.contentOffset I'm resetting either its x or y deciding this way which is the direction in which the UIScrollView should continue scrolling.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    ScrollDirection scrollDirection = [self determineScrollDirectionAxis: scrollView];
    
    if (scrollDirection == ScrollDirectionVertical) {
        NSLog(@"Scrolling direction: vertical");
    } else if (scrollDirection == ScrollDirectionHorizontal) {
        NSLog(@"Scrolling direction: horizontal");
    } else {
        // This is probably crazy movement: diagonal scrolling
        CGPoint newOffset;
        
        if (abs(scrollView.contentOffset.x) > abs(scrollView.contentOffset.y)) {
            newOffset = CGPointMake(scrollView.contentOffset.x, _initialContentOffset.y);
        } else {
            newOffset = CGPointMake(_initialContentOffset.x, scrollView.contentOffset.y);
        }
        
        // Setting the new offset to the scrollView makes it behave like a proper
        // directional lock, that allows you to scroll in only one direction at any given time
        [_scrollView setContentOffset: newOffset];
    }
}

Now let's see how it feels!

It always bounds to only one direction, which is the behaviour I was looking for! Awesome!

You can download the source code of the demo project on GitHub.

Written by Bogdan Constantinescu on
Tagged: objective-c ios