UIScrollView: paging horizontally, scrolling vertically?

IosObjective CUiscrollviewScroll Paging

Ios Problem Overview


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.

Ios Solutions


Solution 1 - Ios

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.

Solution 2 - Ios

This works for me everytime...

scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);

Solution 3 - Ios

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

Solution 4 - Ios

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

Solution 5 - Ios

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);
	}
}

Solution 6 - Ios

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.

Solution 7 - Ios

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.

Solution 8 - Ios

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

Solution 9 - Ios

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

Solution 10 - Ios

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

Solution 11 - Ios

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

Solution 12 - Ios

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

Solution 13 - Ios

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?

Solution 14 - Ios

Need to reset _isHorizontalScroll to NO in touchesEnded and touchesCancelled.

Solution 15 - Ios

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!

Solution 16 - Ios

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

Solution 17 - Ios

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
    }
}

Solution 18 - Ios

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

Solution 19 - Ios

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!

Solution 20 - Ios

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
    }
}

Solution 21 - Ios

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNickDView Question on Stackoverflow
Solution 1 - IosAndrey TarantsovView Answer on Stackoverflow
Solution 2 - IosTonetelView Answer on Stackoverflow
Solution 3 - IosicompotView Answer on Stackoverflow
Solution 4 - IosYildiray MericView Answer on Stackoverflow
Solution 5 - IosJames JView Answer on Stackoverflow
Solution 6 - IosjulianlubenovView Answer on Stackoverflow
Solution 7 - IosBenjaminView Answer on Stackoverflow
Solution 8 - IosMattias WadmanView Answer on Stackoverflow
Solution 9 - IosKurt HorimotoView Answer on Stackoverflow
Solution 10 - IosJeremyView Answer on Stackoverflow
Solution 11 - IosJon ConnerView Answer on Stackoverflow
Solution 12 - IosMobile DeveloperView Answer on Stackoverflow
Solution 13 - IosphilsquaredView Answer on Stackoverflow
Solution 14 - IosPraveen MatanamView Answer on Stackoverflow
Solution 15 - Iosuser422756View Answer on Stackoverflow
Solution 16 - IosChandan Shetty SPView Answer on Stackoverflow
Solution 17 - IosmrkotView Answer on Stackoverflow
Solution 18 - IosTim KeatingView Answer on Stackoverflow
Solution 19 - IosjonprasetyoView Answer on Stackoverflow
Solution 20 - IosMorten Saabye KristensenView Answer on Stackoverflow
Solution 21 - IoszpeskView Answer on Stackoverflow