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
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!
You can download the source code of the demo project on GitHub.