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.
![Imroper UIScrollView direction lock](/assets/direction-lock-uiscrollview/uiscrollview-improper-direction-lock.gif)
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
orScrollDirectionHorizontal
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!
![Proper UIScrollView direction lock](/assets/direction-lock-uiscrollview/uiscrollview-proper-direction-lock.gif)
You can download the source code of the demo project on GitHub.