UIScrollView: paging horizontally, scrolling vertically?
Asked Answered
A

21

91

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.

Alenealenson answered 7/4, 2009 at 23:44 Comment(3)
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?Shied
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?”Pyorrhea
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!Entrain
P
117

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;
}
@end

// 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)
        break;
  return result;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [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];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)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
  else
    [_currentChild touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  if (_isHorizontalScroll)
    [super touchesEnded:touches withEvent:event];
    else {
    [super touchesCancelled:touches withEvent:event];
    [_currentChild touchesEnded:touches withEvent:event];
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
  [super touchesCancelled:touches withEvent:event];
  if (!_isHorizontalScroll)
    [_currentChild touchesCancelled:touches withEvent:event];
}

@end

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 [email protected], I'd happily include it into my ScrollingMadness bag of tricks.

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

Pyorrhea answered 10/5, 2009 at 3:28 Comment(5)
Note that this nested scrollview behaviour works out of the box now in the SDK, no funny business neededRobertaroberto
+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.Kurzawa
Thanks for the great behind-the-scenes explanation! I went with Mattias Wadman's solution below which looks a bit less invasive IMO.Guenevere
It would be great to see this answer updated now that UIScrollView exposes its pan gesture recognizer. (But perhaps that's outside the original scope.)Homeless
Is there a possibility for this post to be updated? Great post and thank you for your input.Nasa
E
56

This works for me everytime...

scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);
Empoverish answered 7/7, 2010 at 18:51 Comment(4)
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.Robertaroberto
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.Simp
Excellent! But can someone explain how this works (setting contentSize height to 1)!Busey
It really does work, but why and how does it work? Can anybody make some sense out of it?Kastner
O
29

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,
                                               self.oldContentOffset.y);
    }
    else
    {
        scrollView.pagingEnabled = NO;
    }
}

- (void) scrollViewDidEndDecelerating: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}
Overshine answered 12/11, 2013 at 12:17 Comment(0)
I
15

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;
    scrollDirection=0;
}

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");
          scrollDirection=1;
       } else {
          NSLog(@"Horitonzal Scrolling");
          scrollDirection=2;
       }
    }
//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;

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (decelerate) {
       scrollDirection=3;
    }
 }
Inductee answered 9/11, 2010 at 15:41 Comment(3)
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 whileRobertaroberto
@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; }Myel
Didn't work for me. Setting the contentOffset in flight seems to destroy the paging behavior. I went with Mattias Wadman's solution.Guenevere
D
13

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);
    }
}
Downtoearth answered 4/3, 2010 at 1:4 Comment(1)
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.Robertaroberto
F
4

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.

Foregoing answered 5/6, 2010 at 19:26 Comment(1)
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.Robertaroberto
A
4

For future reference, I built upon Andrey Tanasov's answer and came up with the following solution that works fine.

First define a variable to store the contentOffset and set it to 0,0 coordinates

var positionScroll = CGPointMake(0, 0)

Now implement the following in the scrollView delegate:

override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
    // Note that we are getting a reference to self.scrollView and not the scrollView referenced passed by the method
    // For some reason, not doing this fails to get the logic to work
    self.positionScroll = (self.scrollView?.contentOffset)!
}

override func scrollViewDidScroll(scrollView: UIScrollView) {

    // Check that contentOffset is not nil
    // We do this because the scrollViewDidScroll method is called on viewDidLoad
    if self.scrollView?.contentOffset != nil {

        // The user wants to scroll on the X axis
        if self.scrollView?.contentOffset.x > self.positionScroll.x || self.scrollView?.contentOffset.x < self.positionScroll.x {

            // Reset the Y position of the scrollView to what it was BEFORE scrolling started
            self.scrollView?.contentOffset = CGPointMake((self.scrollView?.contentOffset.x)!, self.positionScroll.y);
            self.scrollView?.pagingEnabled = true
        }

        // The user wants to scroll on the Y axis
        else {
            // Reset the X position of the scrollView to what it was BEFORE scrolling started
            self.scrollView?.contentOffset = CGPointMake(self.positionScroll.x, (self.scrollView?.contentOffset.y)!);
            self.collectionView?.pagingEnabled = false
        }

    }

}

Hope this helps.

Acetone answered 21/10, 2015 at 12:39 Comment(0)
A
3

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

PagedOrthoScrollView.h

@interface PagedOrthoScrollView : UIScrollView
@end

PagedOrthoScrollView.m

#import "PagedOrthoScrollView.h"

typedef enum {
  PagedOrthoScrollViewLockDirectionNone,
  PagedOrthoScrollViewLockDirectionVertical,
  PagedOrthoScrollViewLockDirectionHorizontal
} PagedOrthoScrollViewLockDirection; 

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

@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)) *
                        CGRectGetHeight(self.bounds));
    } else {
      self.dirLock = PagedOrthoScrollViewLockDirectionVertical;
      self.valueLock = (round(CGRectGetMinX(bounds) / CGRectGetWidth(self.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;
  }

  mx = abs((int)CGRectGetMinX(bounds) % (int)CGRectGetWidth(self.bounds));
  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];
}

@end
Acetylene answered 28/8, 2011 at 13:59 Comment(1)
Works great, also on iOS 7. Thanks!Guenevere
B
3

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).
Benton answered 2/7, 2014 at 18:50 Comment(0)
N
2

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);
Nitride answered 7/8, 2010 at 17:25 Comment(0)
H
2

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;  
}           
Hague answered 12/9, 2011 at 18:8 Comment(0)
B
2

This is improved solution of icompot, which was quite unstable for me. This one works fine and it's easy to implement:

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

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

    float XOffset = fabsf(self.oldContentOffset.x - scrollView.contentOffset.x );
    float YOffset = fabsf(self.oldContentOffset.y - scrollView.contentOffset.y );

        if (scrollView.contentOffset.x != self.oldContentOffset.x && (XOffset >= YOffset) )
        {

            scrollView.pagingEnabled = YES;
            scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x,
                                                   self.oldContentOffset.y);

        }
        else
        {
            scrollView.pagingEnabled = NO;
               scrollView.contentOffset = CGPointMake( self.oldContentOffset.x,
                                                   scrollView.contentOffset.y);
        }

}


- (void) scrollViewDidEndDecelerating: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}
Blok answered 15/7, 2015 at 5:34 Comment(0)
C
2

This is a Swift 5 version of Benjamin's answer

// add this property to your view controller
var positionScroll: CGPoint = .zero

// make sure to set isDirectionalLockEnabled to true on your scroll view
self.scrollView.isDirectionalLockEnabled = true

// then in scrollview/collectionview/tableview delegate
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    positionScroll = scrollView.contentOffset
}

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    // The user wants to scroll on the X axis
    if scrollView.contentOffset.x > positionScroll.x || scrollView.contentOffset.x < positionScroll.x {
        // Reset the Y position of the scrollView to what it was before scrolling started
        scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: positionScroll.y)
        scrollView.isPagingEnabled = true
    } else {
        // The user wants to scroll on the Y axis
        // Reset the X position of the scrollView to what it was before scrolling started
        scrollView.contentOffset = CGPoint(x: positionScroll.x, y: scrollView.contentOffset.y)
        scrollView.isPagingEnabled = false
    }
}
Corybantic answered 31/5, 2021 at 10:39 Comment(0)
C
1

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?

Coquette answered 8/4, 2009 at 0:28 Comment(0)
S
1

Need to reset _isHorizontalScroll to NO in touchesEnded and touchesCancelled.

Syndetic answered 9/10, 2009 at 7:4 Comment(0)
L
1

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
      2.2
      2.3

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!

Landholder answered 17/8, 2010 at 11:2 Comment(1)
don't understand how this could be working for you... could you upload a sample-project?Perlman
M
1

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

Mahayana answered 10/7, 2012 at 9:58 Comment(0)
M
0

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...)

Michiko answered 9/6, 2009 at 6:5 Comment(0)
S
0

For those looking in Swift at @AndreyTarantsov second solution its like this in code:

    var positionScroll:CGFloat = 0

    func scrollViewWillBeginDragging(scrollView: UIScrollView) {
        positionScroll = self.theScroll.contentOffset.x
    }

    func scrollViewDidScroll(scrollView: UIScrollView) {
        if self.theScroll.contentOffset.x > self.positionScroll || self.theScroll.contentOffset.x < self.positionScroll{
             self.theScroll.pagingEnabled = true
        }else{
            self.theScroll.pagingEnabled = false
        }
    }

what we are doing here is to keep the current position just before scrollview gets dragged and then check if either x has increased or decreased compared to the current position of x. If yes, then set the pagingEnabled to true, if no (y has increased/decreased) then set to pagingEnabled to false

Hope that is useful to new comers!

Sharper answered 28/6, 2015 at 12:0 Comment(0)
M
0

I managed to solve this by implementing scrollViewDidBeginDragging(_:) and looking at the velocity on the underlying UIPanGestureRecognizer.

NB: With this solution, every pan that would have been diagonal will be ignored by the scrollView.

override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    super.scrollViewWillBeginDragging(scrollView)
    let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
    if velocity.x != 0 && velocity.y != 0 {
        scrollView.panGestureRecognizer.isEnabled = false
        scrollView.panGestureRecognizer.isEnabled = true
    }
}
Mussman answered 4/9, 2019 at 6:46 Comment(0)
B
-3

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.

Bram answered 7/4, 2009 at 23:47 Comment(1)
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.Clove

© 2022 - 2024 — McMap. All rights reserved.