188

I have a UIScrollView with only horizontal scrolling allowed, and I would like to know which direction (left, right) the user scrolls. What I did was to subclass the UIScrollView and override the touchesMoved method:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesMoved:touches withEvent:event];

    UITouch *touch = [touches anyObject];
    float now = [touch locationInView:self].x;
    float before = [touch previousLocationInView:self].x;
    NSLog(@"%f %f", before, now);
    if (now > before){
        right = NO;
        NSLog(@"LEFT");
    }
    else{
        right = YES;
        NSLog(@"RIGHT");

    }

}

But this method sometimes doesn't get called at all when I move. What do you think?

Shaik Riyaz
  • 10,085
  • 7
  • 48
  • 67
Alex1987
  • 8,819
  • 14
  • 66
  • 90

24 Answers24

396

Determining the direction is fairly straightforward, but keep in mind that the direction can change several times over the course of a gesture. For example, if you have a scroll view with paging turned on and the user swipes to go to the next page, the initial direction could be rightward, but if you have bounce turned on, it will briefly be going in no direction at all and then briefly be going leftward.

To determine the direction, you'll need to use the UIScrollView scrollViewDidScroll delegate. In this sample, I created a variable named lastContentOffset which I use to compare the current content offset with the previous one. If it's greater, then the scrollView is scrolling right. If it's less then the scrollView is scrolling left:

// somewhere in the private class extension
@property (nonatomic, assign) CGFloat lastContentOffset;

// somewhere in the class implementation
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

    ScrollDirection scrollDirection;

    if (self.lastContentOffset > scrollView.contentOffset.x) {
        scrollDirection = ScrollDirectionRight;
    } else if (self.lastContentOffset < scrollView.contentOffset.x) {
        scrollDirection = ScrollDirectionLeft;
    }

    self.lastContentOffset = scrollView.contentOffset.x;

    // do whatever you need to with scrollDirection here.    
}

I'm using the following enum to define direction. Setting the first value to ScrollDirectionNone has the added benefit of making that direction the default when initializing variables:

typedef NS_ENUM(NSInteger, ScrollDirection) {
    ScrollDirectionNone,
    ScrollDirectionRight,
    ScrollDirectionLeft,
    ScrollDirectionUp,
    ScrollDirectionDown,
    ScrollDirectionCrazy,
};
Iulian Onofrei
  • 7,489
  • 8
  • 59
  • 96
memmons
  • 39,202
  • 21
  • 146
  • 181
  • 1
    How can I get lastContentOffset – Dev Nov 15 '12 at 12:27
  • @JasonZhao Heh -- I was getting some strange results when I was testing the code intially because I didn't take into account that the scrollview bounce causes scroll direction to..er..bounce. So, I added that to the enum when I found unexpected results. – memmons Nov 18 '12 at 21:44
  • 1
    @Dev It's in the code `lastContentOffset = scrollView.contentOffset.x;` – memmons Nov 20 '12 at 18:50
  • @akivag29 Good catch on the `lastContentOffset` type. Regarding property vs static variables: either solution is a good choice. I have no real preference. It's a good point. – memmons Feb 28 '14 at 17:29
  • just one little quirk: `- (void)scrollViewDidScroll:(UIScrollView *)sender` should be `- (void)scrollViewDidScroll:(UIScrollView *)scrollView` (**scrollView**) – Hugues BR Mar 05 '14 at 17:50
  • Works like a charm, thanks! I only wanted to know about going up or down, so I didn't need the enum or anything, and just changing .x to .y in all three places was all I needed. – CodyMace Dec 12 '14 at 22:38
  • To account for bounce, e.g. if a user scrolls too far up, when he lets go, the scrollview will bounce back down, incorrectly resulting in direction.down. I used `scrollView.contentOffset.y < 0` to only get "true" direction.up. – Josue Espinosa Jul 06 '15 at 18:53
  • 2
    @JosueEspinosa To get correct scroll direction, you put it in scrollViewWillBeginDragging: method to lastContentOffset setter call. – strawnut Jun 02 '16 at 09:37
75

...I would like to know which direction (left, right) the user scrolls.

In that case, on iOS 5 and above, use the UIScrollViewDelegate to determine the direction of the user's pan gesture:

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{ 
    if ([scrollView.panGestureRecognizer translationInView:scrollView.superview].x > 0) {
        // handle dragging to the right
    } else {
        // handle dragging to the left
    }
}
Khawar
  • 8,881
  • 9
  • 41
  • 65
followben
  • 8,585
  • 4
  • 35
  • 39
  • 1
    could be the best solution, but with really slow start, dx will be equal to 0. – trickster77777 Nov 24 '14 at 04:11
  • Sure it does. This should be upvoted way more as this is more a “native” or proper way mainly because there's need to use any property-stored values. – Michi Jun 03 '16 at 11:40
  • Doesn't this have the same issue as http://stackoverflow.com/a/26192103/62 where it will not work correctly once momentum scrolling kicks in after the user stops panning? – Liron Yahdav Nov 18 '16 at 22:18
61

Using scrollViewDidScroll: is a good way to find the current direction.

If you want to know the direction after the user has finished scrolling, use the following:

@property (nonatomic) CGFloat lastContentOffset;

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

    self.lastContentOffset = scrollView.contentOffset.x;
}

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

    if (self.lastContentOffset > scrollView.contentOffset.x) {
        // moved right if last content offset is greater then current offset
    } else if (self.lastContentOffset < scrollView.contentOffset.x) {
        // moved left if last content offset is less that current offset
    } else {
        // didn't move
    }
}
Justin Tanner
  • 13,409
  • 16
  • 75
  • 99
  • 3
    This is a great addition Justin, however I would like to add one edit. If your scrollview has paging enabled, and you perform a drag that ends up going back to its initial position, the current "else" condition will consider that a "moved left", even though it should be "no change". – Scott Lieberman Apr 10 '13 at 22:07
  • 1
    @ScottLieberman your right, i've updated the code accordingly. – Justin Tanner Jul 14 '13 at 07:18
52

No need to add an extra variable to keep track of this. Just use the UIScrollView's panGestureRecognizer property like this. Unfortunately, this works only if the velocity isn't 0:

CGFloat yVelocity = [scrollView.panGestureRecognizer velocityInView:scrollView].y;
if (yVelocity < 0) {
    NSLog(@"Up");
} else if (yVelocity > 0) {
    NSLog(@"Down");
} else {
    NSLog(@"Can't determine direction as velocity is 0");
}

You can use a combination of x and y components to detect up, down, left and right.

rounak
  • 8,669
  • 3
  • 38
  • 58
44

The solution

func scrollViewDidScroll(scrollView: UIScrollView) {
     if(scrollView.panGestureRecognizer.translationInView(scrollView.superview).y > 0)
     {
         print("up")
     }
    else
    {
         print("down")
    } 
}
Bhumika
  • 866
  • 7
  • 20
davidrelgr
  • 825
  • 1
  • 9
  • 11
22

Swift 4:

For the horizontal scrolling you can simply do :

if scrollView.panGestureRecognizer.translation(in: scrollView.superview).x > 0 {
   print("left")
} else {
   print("right")
}

For vertical scrolling change .x with .y

Community
  • 1
  • 1
Alessandro Ornano
  • 31,579
  • 11
  • 90
  • 115
14

In iOS8 Swift I used this method:

override func scrollViewDidScroll(scrollView: UIScrollView){

    var frame: CGRect = self.photoButton.frame
    var currentLocation = scrollView.contentOffset.y

    if frame.origin.y > currentLocation{
        println("Going up!")
    }else if frame.origin.y < currentLocation{
        println("Going down!")
    }

    frame.origin.y = scrollView.contentOffset.y + scrollHeight
    photoButton.frame = frame
    view.bringSubviewToFront(photoButton)

}

I have a dynamic view which changes locations as the user scrolls so the view can seem like it stayed in the same place on the screen. I am also tracking when user is going up or down.

Here is also an alternative way:

func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    if targetContentOffset.memory.y < scrollView.contentOffset.y {
        println("Going up!")
    } else {
        println("Going down!")
    }
}
Esqarrouth
  • 35,175
  • 17
  • 147
  • 154
8

This is what it worked for me (in Objective-C):

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

        NSString *direction = ([scrollView.panGestureRecognizer translationInView:scrollView.superview].y >0)?@"up":@"down";
        NSLog(@"%@",direction);
    }
neowinston
  • 6,762
  • 9
  • 49
  • 80
7
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {

    CGPoint targetPoint = *targetContentOffset;
    CGPoint currentPoint = scrollView.contentOffset;

    if (targetPoint.y > currentPoint.y) {
        NSLog(@"up");
    }
    else {
        NSLog(@"down");
    }
}
haynar
  • 5,708
  • 7
  • 29
  • 53
Oded Regev
  • 3,235
  • 2
  • 34
  • 49
  • 2
    This method is not called when the value of the scroll view’s pagingEnabled property is YES. – Ako Oct 08 '13 at 22:02
6

In swift:

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    if scrollView.panGestureRecognizer.translation(in: scrollView).y < 0 {
        print("down")
    } else {
        print("up")
    }
}

You can do it also in scrollViewDidScroll.

5

Alternatively, it is possible to observe key path "contentOffset". This is useful when it's not possible for you to set/change the delegate of the scroll view.

[yourScrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

After adding the observer, you could now:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    CGFloat newOffset = [[change objectForKey:@"new"] CGPointValue].y;
    CGFloat oldOffset = [[change objectForKey:@"old"] CGPointValue].y;
    CGFloat diff = newOffset - oldOffset;
    if (diff < 0 ) { //scrolling down
        // do something
    }
}

Do remember to remove the observer when needed. e.g. you could add the observer in viewWillAppear and remove it in viewWillDisappear

xu huanze
  • 269
  • 2
  • 9
4

Here is my solution for behavior like in @followben answer, but without loss with slow start (when dy is 0)

@property (assign, nonatomic) BOOL isFinding;
@property (assign, nonatomic) CGFloat previousOffset;

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    self.isFinding = YES;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (self.isFinding) {
        if (self.previousOffset == 0) {
            self.previousOffset = self.tableView.contentOffset.y;

        } else {
            CGFloat diff = self.tableView.contentOffset.y - self.previousOffset;
            if (diff != 0) {
                self.previousOffset = 0;
                self.isFinding = NO;

                if (diff > 0) {
                    // moved up
                } else {
                    // moved down
                }
            }
        }
    }
}
2
//Vertical detection
    var lastVelocityYSign = 0
            func scrollViewDidScroll(_ scrollView: UIScrollView) {
                let currentVelocityY =  scrollView.panGestureRecognizer.velocity(in: scrollView.superview).y
                let currentVelocityYSign = Int(currentVelocityY).signum()
                if currentVelocityYSign != lastVelocityYSign &&
                    currentVelocityYSign != 0 {
                    lastVelocityYSign = currentVelocityYSign
                }
                if lastVelocityYSign < 0 {
                    print("SCROLLING DOWN")
                } else if lastVelocityYSign > 0 {
                    print("SCOLLING UP")
                }
            }

Answer from Mos6y https://medium.com/@Mos6yCanSwift/swift-ios-determine-scroll-direction-d48a2327a004

alexherm
  • 1,255
  • 13
  • 25
slimas
  • 21
  • 4
1

I checked some of the answer and elaborated on AnswerBot answer by wrapping everything in a drop in UIScrollView category. The "lastContentOffset" is saved inside the uiscrollview instead and then its just a matter of calling :

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  [scrollView setLastContentOffset:scrollView.contentOffset];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
  if (scrollView.scrollDirectionX == ScrollDirectionRight) {
    //Do something with your views etc
  }
  if (scrollView.scrollDirectionY == ScrollDirectionUp) {
    //Do something with your views etc
  }
}

Source code at https://github.com/tehjord/UIScrollViewScrollingDirection

Bjergsen
  • 187
  • 17
1

I prefer to do some filtering, based on @memmons's answer

In Objective-C:

// in the private class extension
@property (nonatomic, assign) CGFloat lastContentOffset;

// in the class implementation
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

    if (fabs(self.lastContentOffset - scrollView.contentOffset.x) > 20 ) {
        self.lastContentOffset = scrollView.contentOffset.x;
    }

    if (self.lastContentOffset > scrollView.contentOffset.x) {
        //  Scroll Direction Left
        //  do what you need to with scrollDirection here.
    } else {
        //  omitted 
        //  if (self.lastContentOffset < scrollView.contentOffset.x)

        //  do what you need to with scrollDirection here.
        //  Scroll Direction Right
    } 
}

When tested in - (void)scrollViewDidScroll:(UIScrollView *)scrollView:

NSLog(@"lastContentOffset: --- %f,   scrollView.contentOffset.x : --- %f", self.lastContentOffset, scrollView.contentOffset.x);

img

self.lastContentOffset changes very fast, the value gap is nearly 0.5f.

It is not necessary.

And occasionally, when handled in accurate condition, your orientation maybe get lost. (implementation statements skipped sometimes)

such as :

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

    CGFloat viewWidth = scrollView.frame.size.width;

    self.lastContentOffset = scrollView.contentOffset.x;
    // Bad example , needs value filtering

    NSInteger page = scrollView.contentOffset.x / viewWidth;

    if (page == self.images.count + 1 && self.lastContentOffset < scrollView.contentOffset.x ){
          //  Scroll Direction Right
          //  do what you need to with scrollDirection here.
    }
   ....

In Swift 4:

var lastContentOffset: CGFloat = 0

func scrollViewDidScroll(_ scrollView: UIScrollView) {

     if (abs(lastContentOffset - scrollView.contentOffset.x) > 20 ) {
         lastContentOffset = scrollView.contentOffset.x;
     }

     if (lastContentOffset > scrollView.contentOffset.x) {
          //  Scroll Direction Left
          //  do what you need to with scrollDirection here.
     } else {
         //  omitted
         //  if (self.lastContentOffset < scrollView.contentOffset.x)

         //  do what you need to with scrollDirection here.
         //  Scroll Direction Right
    }
}
black_pearl
  • 1,548
  • 13
  • 25
0

When paging is turned on,you could use these code.

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    self.lastPage = self.currentPage;
    CGFloat pageWidth = _mainScrollView.frame.size.width;
    self.currentPage = floor((_mainScrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
    if (self.lastPage < self.currentPage) {
        //go right
        NSLog(@"right");
    }else if(self.lastPage > self.currentPage){
        //go left
        NSLog(@"left");
    }else if (self.lastPage == self.currentPage){
        //same page
        NSLog(@"same page");
    }
}
Perry
  • 451
  • 5
  • 6
0

Codes explains itself I guess. CGFloat difference1 and difference2 declared in same class private interface. Good if contentSize stays same.

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

        CGFloat contentOffSet = scrollView.contentOffset.y;
        CGFloat contentHeight = scrollView.contentSize.height;

        difference1 = contentHeight - contentOffSet;

        if (difference1 > difference2) {
            NSLog(@"Up");
        }else{
            NSLog(@"Down");
        }

        difference2 = contentHeight - contentOffSet;

       }
user2511630
  • 2,564
  • 1
  • 14
  • 15
0

Ok so for me this implementation is working really good:

@property (nonatomic, assign) CGPoint lastContentOffset;


- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    _lastContentOffset.x = scrollView.contentOffset.x;
    _lastContentOffset.y = scrollView.contentOffset.y;

}


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

    if (_lastContentOffset.x < (int)scrollView.contentOffset.x) {
        // moved right
        NSLog(@"right");
    }
    else if (_lastContentOffset.x > (int)scrollView.contentOffset.x) {
        // moved left
        NSLog(@"left");

    }else if (_lastContentOffset.y<(int)scrollView.contentOffset.y){
        NSLog(@"up");

    }else if (_lastContentOffset.y>(int)scrollView.contentOffset.y){
        NSLog(@"down");
        [self.txtText resignFirstResponder];

    }
}

So this will fire textView to dismiss after drag ends

user2021505
  • 234
  • 1
  • 12
0
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
NSLog(@"px %f py %f",velocity.x,velocity.y);}

Use this delegate method of scrollview.

If y co-ordinate of velocity is +ve scroll view scrolls downwards and if it is -ve scrollview scrolls upwards. Similarly left and right scroll can be detected using x co-ordinate.

Nilesh Tupe
  • 7,053
  • 5
  • 23
  • 30
0

Short & Easy would be, just check the velocity value, if its greater than zero then its scrolling left else right:

func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    var targetOffset = Float(targetContentOffset.memory.x)
    println("TargetOffset: \(targetOffset)")
    println(velocity)

    if velocity.x < 0 {
        scrollDirection = -1 //scrolling left
    } else {
        scrollDirection = 1 //scrolling right
    }
}
iDilip
  • 1,012
  • 11
  • 25
0

If you work with UIScrollView and UIPageControl, this method will also change the PageControl's page view.

  func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    let targetOffset = targetContentOffset.memory.x
    let widthPerPage = scrollView.contentSize.width / CGFloat(pageControl.numberOfPages)

    let currentPage = targetOffset / widthPerPage
    pageControl.currentPage = Int(currentPage)
}

Thanks @Esq 's Swift code.

charles.cc.hsu
  • 569
  • 8
  • 14
0

Swift 2.2 Simple solution which tracks single and multiple directions without any loss.

  // Keep last location with parameter
  var lastLocation:CGPoint = CGPointZero

  // We are using only this function so, we can
  // track each scroll without lose anyone
  override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
    let currentLocation = scrollView.contentOffset

    // Add each direction string
    var directionList:[String] = []

    if lastLocation.x < currentLocation.x {
      //print("right")
      directionList.append("Right")
    } else if lastLocation.x > currentLocation.x {
      //print("left")
      directionList.append("Left")
    }

    // there is no "else if" to track both vertical
    // and horizontal direction
    if lastLocation.y < currentLocation.y {
      //print("up")
      directionList.append("Up")
    } else if lastLocation.y > currentLocation.y {
      //print("down")
      directionList.append("Down")
    }

    // scrolled to single direction
    if directionList.count == 1 {
      print("scrolled to \(directionList[0]) direction.")
    } else if directionList.count > 0  { // scrolled to multiple direction
      print("scrolled to \(directionList[0])-\(directionList[1]) direction.")
    }

    // Update last location after check current otherwise,
    // values will be same
    lastLocation = scrollView.contentOffset
  }
fatihyildizhan
  • 7,103
  • 5
  • 54
  • 74
0

In all upper answers two major ways to solve the problem is using panGestureRecognizer or contentOffset. Both methods have their cons and pros.

Method 1: panGestureRecognizer

When you use panGestureRecognizer like what @followben suggested, if you don't want to programmatically scroll your scroll view, it works properly.

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{ 
    if ([scrollView.panGestureRecognizer translationInView:scrollView.superview].x > 0) {
        // handle dragging to the right
    } else {
        // handle dragging to the left
    }
}

Cons

But if you move scroll view with following code, upper code can not recognize it:

setContentOffset(CGPoint(x: 100, y: 0), animation: false)

Method 2: contentOffset

var lastContentOffset: CGPoint = CGPoint.zero

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if (self.lastContentOffset.x > scrollView.contentOffset.x) {
        // scroll to right
    } else if self.lastContentOffset.x < scrollView.contentOffset.x {
        // scroll to left
    }
    self.lastContentOffset = self.scrollView.contentOffset
}

Cons

If you want to change contentOffset programmatically, during scroll (like when you want to create infinite scroll), this method makes problem, because you might change contentOffset during changing content views places and in this time, upper code jump in that you scroll to right or left.

Esmaeil
  • 181
  • 2
  • 7
0

Swift 5

More clean solution with enum for vertical scrolling.

enum ScrollDirection {
    case up, down
}

var scrollDirection: ScrollDirection? {
    if scrollView.panGestureRecognizer.translation(in: scrollView.superview).y > 0 {
        return .up
    } else if scrollView.panGestureRecognizer.translation(in: scrollView.superview).y < 0 {
        return .down
    } else {
        return nil
    }
}

Usage

switch scrollDirection {
case .up:   print("up")
case .down: print("down")
default:    print("no scroll")
}
Mofawaw
  • 2,576
  • 1
  • 12
  • 37