Take the 2-minute tour ×

How can I force a UIScrollView in which paging and scrolling are on to only move vertically or horizontally at a given moment?

My understanding is that the directionalLockEnabled property should achieve this, but a diagonal swipe still causes the view to scroll diagonally instead of restricting motion to a single axis.

Edit: to be clearer, I'd like to allow the user to scroll horizontally OR vertically, but not both simultaneously.

share|improve this question
Do you want to constrain a view to be scrolled ONLY Horiz/Vert or do you want the user to scroll Horiz OR Vert, but not both simultaneously? –  Will Hartung Apr 8 '09 at 0:32
Can you do me a favour and change the title so that it looks a bit more interesting and non-trivial? Something like “UIScrollView: paging horizontally, scrolling vertically?” –  Andrey Tarantsov May 10 '09 at 3:32
Unfortunately, I posted the question as an unregistered user on a computer I no longer have access to (before registering under the same name when I got home), which means I can't edit it. This should also explain me following-up lower down instead of editing the original question. Sorry for breaking Stack etiquette this time! –  Nick May 10 '09 at 9:58

16 Answers 16

You're in a very tough situation, I must say.

Note that you need to use a UIScrollView with pagingEnabled=YES to switch between pages, but you need pagingEnabled=NO to scroll vertically.

There are 2 possible strategies. I don't know which one will work / is easier to implement, so try both.

First: nested UIScrollViews. Frankly, I'm yet to see a person who has got this to work. However I have not tried hard enough personally, and my practise shows that when you do try hard enough, you can make UIScrollView do anything you want.

So the strategy is to let the outer scroll view only handle horizontal scrolling, and inner scroll views to only handle vertical scrolling. To accomplish that, you must know how UIScrollView works internally. It overrides hitTest method and always returns itself, so that all touch events go into UIScrollView. Then inside touchesBegan, touchesMoved etc it checks if it's interested in the event, and either handles or passes it on to the inner components.

To decide if the touch is to be handled or to be forwarded, UIScrollView starts a timer when you first touch it:

  • If you haven't moved your finger significantly within 150ms, it passes the event on to the inner view.

  • If you have moved your finger significantly within 150ms, it starts scrolling (and never passes the event to the inner view).

    Note how when you touch a table (which is a subclass of scroll view) and start scrolling immediately, the row that you touched is never highlighted.

  • If you have not moved your finger significantly within 150ms and UIScrollView started passing the events to the inner view, but then you have moved the finger far enough for the scrolling to begin, UIScrollView calls touchesCancelled on the inner view and starts scrolling.

    Note how when you touch a table, hold your finger a bit and then start scrolling, the row that you touched is highlighted first, but de-highlighted afterwards.

These sequence of events can be altered by configuration of UIScrollView:

  • If delaysContentTouches is NO, then no timer is used — the events immediately go to the inner control (but then are canceled if you move your finger far enough)
  • If cancelsTouches is NO, then once the events are sent to a control, scrolling will never happen.

Note that it is UIScrollView that receives all touchesBegin, touchesMoved, touchesEnded and touchesCanceled events from CocoaTouch (because its hitTest tells it to do so). It then forwards them to the inner view if it wants to, as long as it wants to.

Now that you know everything about UIScrollView, you can alter its behavior. I can bet you want to give preference to vertical scrolling, so that once the user touches the view and starts moving his finger (even slightly), the view starts scrolling in vertical direction; but when the user moves his finger in horizontal direction far enough, you want to cancel vertical scrolling and start horizontal scrolling.

You want to subclass your outer UIScrollView (say, you name your class RemorsefulScrollView), so that instead of the default behaviour it immediately forwards all events to the inner view, and only when significant horizontal movement is detected it scrolls.

How to do make RemorsefulScrollView behave that way?

  • It looks like disabling vertical scrolling and setting delaysContentTouches to NO should make nested UIScrollViews to work. Unfortunately, it does not; UIScrollView appears to do some additional filtering for fast motions (which cannot be disabled), so that even if UIScrollView can only be scrolled horizontally, it will always eat up (and ignore) fast enough vertical motions.

    The effect is so severe that vertical scrolling inside a nested scroll view is unusable. (It appears that you have got exactly this setup, so try it: hold a finger for 150ms, and then move it in vertical direction — nested UIScrollView works as expected then!)

  • This means you cannot use UIScrollView's code for event handling; you have to override all four touch handling methods in RemorsefulScrollView and do your own processing first, only forwarding the event to super (UIScrollView) if you have decided to go with horizontal scrolling.

  • However you have to pass touchesBegan to UIScrollView, because you want it to remember a base coordinate for future horizontal scrolling (if you later decide it is a horizontal scrolling). You won't be able to send touchesBegan to UIScrollView later, because you cannot store the touches argument: it contains objects that will be mutated before the next touchesMoved event, and you cannot reproduce the old state.

    So you have to pass touchesBegan to UIScrollView immediately, but you will hide any further touchesMoved events from it until you decide to scroll horizontally. No touchesMoved means no scrolling, so this initial touchesBegan will do no harm. But do set delaysContentTouches to NO, so that no additional surprise timers interfere.

    (Offtopic — unlike you, UIScrollView can store touches properly and can reproduce and forward the original touchesBegan event later. It has an unfair advantage of using unpublished APIs, so can clone touch objects before they are mutated.)

  • Given that you always forward touchesBegan, you also have to forward touchesCancelled and touchesEnded. You have to turn touchesEnded into touchesCancelled, however, because UIScrollView would interpret touchesBegan, touchesEnded sequence as a touch-click, and would forward it to the inner view. You are already forwarding the proper events yourself, so you never want UIScrollView to forward anything.

Basically here's pseudocode for what you need to do. For simplicity, I never allow horizontal scrolling after multitouch event has occurred.

// RemorsefulScrollView.h

@interface RemorsefulScrollView : UIScrollView {
  CGPoint _originalPoint;
  BOOL _isHorizontalScroll, _isMultitouch;
  UIView *_currentChild;

// RemorsefulScrollView.m

// the numbers from an example in Apple docs, may need to tune them
#define kThresholdX 12.0f
#define kThresholdY 4.0f

@implementation RemorsefulScrollView

- (id)initWithFrame:(CGRect)frame {
  if (self = [super initWithFrame:frame]) {
    self.delaysContentTouches = NO;
  return self;

- (id)initWithCoder:(NSCoder *)coder {
  if (self = [super initWithCoder:coder]) {
    self.delaysContentTouches = NO;
  return self;

- (UIView *)honestHitTest:(CGPoint)point withEvent:(UIEvent *)event {
  UIView *result = nil;
  for (UIView *child in self.subviews)
    if ([child pointInside:point withEvent:event])
      if ((result = [child hitTest:point withEvent:event]) != nil)
  return result;

    [super touchesBegan:touches withEvent:event]; // always forward touchesBegan -- there's no way to forward it later
    if (_isHorizontalScroll)
      return; // UIScrollView is in charge now
    if ([touches count] == [[event touchesForView:self] count]) { // initial touch
      _originalPoint = [[touches anyObject] locationInView:self];
    _currentChild = [self honestHitTest:_originalPoint withEvent:event];
    _isMultitouch = NO;
  _isMultitouch |= ([[event touchesForView:self] count] > 1);
  [_currentChild touchesBegan:touches withEvent:event];

  if (!_isHorizontalScroll && !_isMultitouch) {
    CGPoint point = [[touches anyObject] locationInView:self];
    if (fabsf(_originalPoint.x - point.x) > kThresholdX && fabsf(_originalPoint.y - point.y) < kThresholdY) {
      _isHorizontalScroll = YES;
      [_currentChild touchesCancelled:[event touchesForView:self] withEvent:event]
  if (_isHorizontalScroll)
    [super touchesMoved:touches withEvent:event]; // UIScrollView only kicks in on horizontal scroll
    [_currentChild touchesMoved:touches withEvent:event];

  if (_isHorizontalScroll)
    [super touchesEnded:touches withEvent:event];
    else {
    [super touchesCancelled:touches withEvent:event];
    [_currentChild touchesEnded:touches withEvent:event];

  [super touchesCancelled:touches withEvent:event];
  if (!_isHorizontalScroll)
    [_currentChild touchesCancelled:touches withEvent:event];


I have not tried to run or even to compile this (and typed the whole class in a plain text editor), but you can start with the above and hopefully get it working.

The only hidden catch I see is that if you add any non-UIScrollView child views to RemorsefulScrollView, the touch events you forward to a child may arrive back to you via responder chain, if the child does not always handle touches like UIScrollView does. A bullet-proof RemorsefulScrollView implementation would protect against touchesXxx reentry.

Second strategy: If due to some reason nested UIScrollViews do not work out or prove too hard to get right, you can try to get along with just one UIScrollView, switching its pagingEnabled property on the fly from your scrollViewDidScroll delegate method.

To prevent diagonal scrolling, you should first try remembering contentOffset in scrollViewWillBeginDragging, and checking and resetting contentOffset inside scrollViewDidScroll if you detect a diagonal movement. Another strategy to try is to reset contentSize to only enable scrolling in one direction, once you decide which direction the user's finger is going. (UIScrollView seems pretty forgiving about fiddling with contentSize and contentOffset from its delegate methods.)

If that does not work either or results in sloppy visuals, you have to override touchesBegan, touchesMoved etc and not forward diagonal movement events to UIScrollView. (The user experience will be suboptimal in this case however, because you will have to ignore diagonal movements instead of forcing them into a single direction. If you're feeling really adventurous, you can write your own UITouch lookalike, something like RevengeTouch. Objective-C is plain old C, and there's nothing more ducktypeful in the world than C; as long as noone checks the real class of the objects, which I believe noone does, you can make any class look like any other class. This opens up a possibility to synthesize any touches you want, with any coordinates you want.)

Backup strategy: there's TTScrollView, a sane reimplementation of UIScrollView in Three20 library. Unfortunately it feels very unnatural and non-iphonish to the user. But if every attempt of using UIScrollView fails, you can fall back to a custom-coded scroll view. I do recommend against it if at all possible; using UIScrollView ensures you are getting the native look-and-feel, no matter how it evolves in future iPhone OS versions.

Okay, this little essay got a little bit too long. I'm just still into UIScrollView games after the work on ScrollingMadness several days ago.

P.S. If you get any of these working and feel like sharing, please e-mail me the relevant code at andreyvit@gmail.com, I'd happily include it into my ScrollingMadness bag of tricks.

P.P.S. Adding this little essay to ScrollingMadness README.

share|improve this answer
Note that this nested scrollview behaviour works out of the box now in the SDK, no funny business needed –  andygeers May 1 '11 at 8:41
+1'd andygeers comment, then realized it's not accurate; you can still "break" the regular UIScrollView by dragging diagonally from the starting point and then you've "pulled the scroller lose" and it will ignore directional lock until you release and end up god knows where. –  Kalle Jul 21 '11 at 17:32
Thanks for the great behind-the-scenes explanation! I went with Mattias Wadman's solution below which looks a bit less invasive IMO. –  Ortwin Gentz Feb 5 at 18:01

This works for me everytime...

scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);
share|improve this answer
For anybody else stumbling upon this question: I think this answer was posted before the edit that clarified the question. This will allow you to only scroll horizontally, it doesn't address the issue of being able to scroll vertically or horizontally but not diagonally. –  andygeers Apr 30 '11 at 9:33
Works like a charm for me. I have a very long horizontal scrollView with paging, and a UIWebView on each page, which handles the vertical scrolling I need. –  Tom Redman Jul 18 '12 at 15:40
Excellent! But can someone explain how this works (setting contentSize height to 1)! –  Praveen Jun 15 '13 at 7:23
It really does work, but why and how does it work? Can anybody make some sense out of it? –  Marko Franceković Oct 26 '13 at 23:39

I used following method to solve this problem, hope it helps you.

First define following variables in your controllers header file.

CGPoint startPos;
int     scrollDirection;

startPos will keep the contentOffset value when your delegate receives scrollViewWillBeginDragging message. So in this method we do this;

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    startPos = scrollView.contentOffset;

then we use these values to determine users intended scroll direction in scrollViewDidScroll message.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{

   if (scrollDirection==0){//we need to determine direction
       //use the difference between positions to determine the direction.
       if (abs(startPos.x-scrollView.contentOffset.x)<abs(startPos.y-scrollView.contentOffset.y)){          
          NSLog(@"Vertical Scrolling");
       } else {
          NSLog(@"Horitonzal Scrolling");
//Update scroll position of the scrollview according to detected direction.     
    if (scrollDirection==1) {
       [scrollView setContentOffset:CGPointMake(startPos.x,scrollView.contentOffset.y) animated:NO];
    } else if (scrollDirection==2){
       [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x,startPos.y) animated:NO];

finally we have to stop all update operations when user end dragging;

    if (decelerate) {
share|improve this answer
Hmm... Actually, after more investigation - it works pretty well, but the trouble is that it loses control of the direction during the deceleration phase - so if you start off moving your finger diagonally then it will appear to only move horizontally or vertically until you let go at which point it will decelerate in a diagonal direction for a while –  andygeers Apr 30 '11 at 9:25
@andygeers, it might work better if - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (decelerate) { scrollDirection=3; } } was changed to - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (!decelerate) { scrollDirection=0; } } and - (void)scrollViewDidEndDecelerating{ scrollDirection=0; } –  cduck Jun 23 '11 at 19:46
Didn't work for me. Setting the contentOffset in flight seems to destroy the paging behavior. I went with Mattias Wadman's solution. –  Ortwin Gentz Feb 5 at 17:59

I might be gravedigging here, but I stumbled onto this post today as I was trying to solve the same problem. Here's my solution, seems to be working great.

I want to display a screenful of content, but allow the user to scroll to one of 4 other screenfuls, up down left and right. I have a single UIScrollView with size 320x480 and a contentSize of 960x1440. The content offset starts at 320,480. In scrollViewDidEndDecelerating:, I redraw the 5 views (center views and the 4 around it), and reset the content offset to 320,480.

Here's the meat of my solution, the scrollViewDidScroll: method.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
    if (!scrollingVertically && ! scrollingHorizontally)
        int xDiff = abs(scrollView.contentOffset.x - 320);
        int yDiff = abs(scrollView.contentOffset.y - 480);

        if (xDiff > yDiff)
            scrollingHorizontally = YES;
        else if (xDiff < yDiff)
            scrollingVertically = YES;

    if (scrollingHorizontally)
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, 480);
    else if (scrollingVertically)
        scrollView.contentOffset = CGPointMake(320, scrollView.contentOffset.y);
share|improve this answer
You will probably find that this code doesn't "decelerate" nicely when the user lets go of the scroll view - it will stop dead. If you don't want deceleration this might be fine for you. –  andygeers Apr 30 '11 at 10:19

For the lazy people like me:

Set scrollview.directionalLockEnabled to YES

- (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView
    self.oldContentOffset = scrollView.contentOffset;

- (void) scrollViewDidScroll: (UIScrollView *) scrollView
    if (scrollView.contentOffset.x != self.oldContentOffset.x)
        scrollView.pagingEnabled = YES;
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x,
        scrollView.pagingEnabled = NO;

- (void) scrollViewDidEndDecelerating: (UIScrollView *) scrollView
    self.oldContentOffset = scrollView.contentOffset;
share|improve this answer

Guys it is as simple as making the subview height the same as the scroll view height and the content size height. Then the vertical scrolling is disabled.

share|improve this answer

Here is my implementation of a paged UIScrollView that only allow orthogonal scrolls. Be sure to set pagingEnabled to YES.


@interface PagedOrthoScrollView : UIScrollView


#import "PagedOrthoScrollView.h"

typedef enum {
} PagedOrthoScrollViewLockDirection; 

@interface PagedOrthoScrollView ()
@property(nonatomic, assign) PagedOrthoScrollViewLockDirection dirLock;
@property(nonatomic, assign) CGFloat valueLock;

@implementation PagedOrthoScrollView
@synthesize dirLock;
@synthesize valueLock;

- (id)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self == nil) {
    return self;

  self.dirLock = PagedOrthoScrollViewLockDirectionNone;
  self.valueLock = 0;

  return self;

- (void)setBounds:(CGRect)bounds {
  int mx, my;

  if (self.dirLock == PagedOrthoScrollViewLockDirectionNone) {
    // is on even page coordinates, set dir lock and lock value to closest page 
    mx = abs((int)CGRectGetMinX(bounds) % (int)CGRectGetWidth(self.bounds));
    if (mx != 0) {
      self.dirLock = PagedOrthoScrollViewLockDirectionHorizontal;
      self.valueLock = (round(CGRectGetMinY(bounds) / CGRectGetHeight(self.bounds)) *
    } else {
      self.dirLock = PagedOrthoScrollViewLockDirectionVertical;
      self.valueLock = (round(CGRectGetMinX(bounds) / CGRectGetWidth(self.bounds)) *

    // show only possible scroll indicator
    self.showsVerticalScrollIndicator = dirLock == PagedOrthoScrollViewLockDirectionVertical;
    self.showsHorizontalScrollIndicator = dirLock == PagedOrthoScrollViewLockDirectionHorizontal;

  if (self.dirLock == PagedOrthoScrollViewLockDirectionHorizontal) {
    bounds.origin.y = self.valueLock;
  } else {
    bounds.origin.x = self.valueLock;

  my = abs((int)CGRectGetMinY(bounds) % (int)CGRectGetHeight(self.bounds));

  if (mx == 0 && my == 0) {
    // is on even page coordinates, reset lock
    self.dirLock = PagedOrthoScrollViewLockDirectionNone;

  [super setBounds:bounds];

share|improve this answer
Works great, also on iOS 7. Thanks! –  Ortwin Gentz Feb 5 at 17:56

What I did was create a paging UIScrollView with a frame the size of the view screen and set the content size to the width needed for all of my content and a height of 1 (thanks Tonetel). Then I created menu UIScrollView pages with frames set to each page, the size of the view screen, and set the content size of each to the width of the screen and the necessary height for each one. Then I simply added each of the menu pages to the paging view. Make sure paging is enabled on the paging scroll view but disabled on the menu views (should be disabled by default) and you should be good to go.

The paging scroll view now scrolls horizontally, but not vertically because of the content height. Each page scrolls vertically but not horizontally because of its content size constraints as well.

Here's the code if that explanation left something to be desired:

UIScrollView *newPagingScrollView =  [[UIScrollView alloc] initWithFrame:self.view.bounds]; 
[newPagingScrollView setPagingEnabled:YES];
[newPagingScrollView setShowsVerticalScrollIndicator:NO];
[newPagingScrollView setShowsHorizontalScrollIndicator:NO];
[newPagingScrollView setDelegate:self];
[newPagingScrollView setContentSize:CGSizeMake(self.view.bounds.size.width * NumberOfDetailPages, 1)];
[self.view addSubview:newPagingScrollView];

float pageX = 0;

for (int i = 0; i < NumberOfDetailPages; i++)
    CGRect pageFrame = (CGRect) 
        .origin = CGPointMake(pageX, pagingScrollView.bounds.origin.y), 
        .size = pagingScrollView.bounds.size
    UIScrollView *newPage = [self createNewPageFromIndex:i ToPageFrame:pageFrame]; // newPage.contentSize custom set in here        
    [pagingScrollView addSubview:newPage];

    pageX += pageFrame.size.width;  
share|improve this answer

Thanks Tonetel. I slightly modified your approach, but it was exactly what I needed to prevent horizontal scrolling.

self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, 1);
share|improve this answer

From the docs:

"the default value is NO, which means that scrolling is permitted in both horizontal and vertical directions. If the value is YES and the user begins dragging in one general direction (horizontally or vertically), the scroll view disables scrolling in the other direction."

I think the important part is "if... the user begins dragging in one general direction". So if they begin dragging diagonally this doesn't kick in. Not that these docs are always reliable when it comes to interpretation - but that does seem to fit with what you are seeing.

This actually seems sensible. I have to ask - why do you want to restrict to only horizontal or only vertical at any time? Perhaps UIScrollView is not the tool for you?

share|improve this answer

My view consists of 10 horizontal views, and some of those views consist of multiple vertical views, so you'll get something like this:

1.0   2.0   3.1    4.1
      2.1   3.1

With paging enabled on both horizontal and vertical axis

The view also has the following attributes:

[self setDelaysContentTouches:NO];
[self setMultipleTouchEnabled:NO];
[self setDirectionalLockEnabled:YES];

Now to prevent diagonal scrolling do this:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    int pageWidth = 768;
    int pageHeight = 1024;
    int subPage =round(self.contentOffset.y / pageHeight);
    if ((int)self.contentOffset.x % pageWidth != 0 && (int)self.contentOffset.y % pageHeight != 0) {
        [self setContentOffset:CGPointMake(self.contentOffset.x, subPage * pageHeight];

Works like a charme for me!

share|improve this answer
don't understand how this could be working for you... could you upload a sample-project? –  hfossli Sep 10 '10 at 22:08

Need to reset _isHorizontalScroll to NO in touchesEnded and touchesCancelled.

share|improve this answer

If you are having a big scrollview... and you want to restrict the diagonal scrolling http://chandanshetty01.blogspot.in/2012/07/restricting-diagonal-scrolling-in.html

share|improve this answer
This works like a charm. Thanks! –  Borut Tomazin Sep 12 at 9:41

I've also had to solve this problem, and while Andrey Tarantsov's answer definitely has a lot of useful information for understanding how UIScrollViews work, I feel that the solution is a bit over-complicated. My solution, which doesn't involve any subclassing or touch forwarding, is as follows:

  1. Create two nested scroll views, one for each direction.
  2. Create two dummy UIPanGestureRecognizers, again one for each direction.
  3. Create failure dependencies between the UIScrollViews' panGestureRecognizer properties and the dummy UIPanGestureRecognizer corresponding to the direction perpendicular to your UIScrollView's desired scrolling direction by using UIGestureRecognizer's -requireGestureRecognizerToFail: selector.
  4. In the -gestureRecognizerShouldBegin: callbacks for your dummy UIPanGestureRecognizers, calculate the initial direction of the pan using the gesture's -translationInView: selector (your UIPanGestureRecognizers won't call this delegate callback until your touch has translated enough to register as a pan). Allow or disallow your gesture recognizer to begin depending on the calculated direction, which in turn should control whether or not the perpendicular UIScrollView pan gesture recognizer will be allowed to begin.
  5. In -gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:, don't allow your dummy UIPanGestureRecognizers to run alongside scrolling (unless you have some extra behavior you wanted to add).
share|improve this answer

If you haven't tried doing this in the 3.0 beta, I recommend you give it a shot. I am currently using a series of table views inside a scroll view, and it works just as expected. (Well, the scrolling does, anyway. Found this question when looking for a fix to a totally different issue...)

share|improve this answer

If you are using interface builder to design your interface - this could be done quite easily.

In interface builder simply click on the UIScrollView and select the option (in the inspector) that limits it to only one way scrolling.

share|improve this answer
There are no options in IB to do this. The two options you may be referring to have to do with display of the scroll bar, not the actual scrolling. Furthermore, the Direction Lock only locks the direction once the scrolling has begun. –  Sophtware May 22 '10 at 14:59

Your Answer

