Finding the direction of scrolling in a UIScrollView?
Asked Answered
F

24

198

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?

Famous answered 30/3, 2010 at 8:11 Comment(2)
See my response below -- you should be using the scroll view delegates to do this.Laris
best answer here: #11263083Scarborough
L
400

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,
};
Laris answered 1/11, 2010 at 21:4 Comment(8)
How can I get lastContentOffsetBritzka
@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.Laris
@Britzka It's in the code lastContentOffset = scrollView.contentOffset.x;Laris
@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.Laris
just one little quirk: - (void)scrollViewDidScroll:(UIScrollView *)sender should be - (void)scrollViewDidScroll:(UIScrollView *)scrollView (scrollView)Sororate
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.Inclinable
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.Restoration
@JosueEspinosa To get correct scroll direction, you put it in scrollViewWillBeginDragging: method to lastContentOffset setter call.Howze
L
80

...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
    }
}
Lieberman answered 16/2, 2014 at 3:42 Comment(3)
could be the best solution, but with really slow start, dx will be equal to 0.Cusack
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.Ambrogio
Doesn't this have the same issue as https://mcmap.net/q/127590/-finding-the-direction-of-scrolling-in-a-uiscrollview where it will not work correctly once momentum scrolling kicks in after the user stops panning?Kristoferkristoffer
F
64

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
    }
}
Franfranc answered 8/2, 2012 at 21:24 Comment(2)
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".Indenture
@ScottLieberman your right, i've updated the code accordingly.Franfranc
V
56

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.

Venous answered 4/10, 2014 at 10:54 Comment(4)
unfortunately this just works while dragging. velocity is 0 when touches are removed, even while still scrolling.Cree
Nice +1 . else part Velocity 0 because, user is not dragging anymore, so gesture velocity is 0Asher
You can store direction in variable. Change only when velocity != 0. For me worksCalamint
Works with scrollViewWillBeginDragging, awesomeTopology
E
48

The solution

func scrollViewDidScroll(scrollView: UIScrollView) {
     if(scrollView.panGestureRecognizer.translationInView(scrollView.superview).y > 0)
     {
         print("up")
     }
    else
    {
         print("down")
    } 
}
Elizabetelizabeth answered 4/2, 2016 at 23:45 Comment(3)
Give it a try with velocityInView instead of translationInView. This solves the problem that arises when changing direction in the same movement.Marlo
This solution is working perfect. The most accepted answer is not working some times when i go to next screen and come back perform scroll up and down operations.Auscultate
The best, thnx!Ashlar
S
26

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

Silda answered 28/1, 2018 at 12:10 Comment(0)
S
15

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!")
    }
}
Shawna answered 30/12, 2014 at 10:3 Comment(2)
Use x instead of y ?Shawna
Actually I want to detect it for UiCollectionView. I have horizontal scrolling. So i will detect in crollViewDidEndDecelerating right ?Drennan
C
9

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);
    }
Coaster answered 5/7, 2016 at 12:30 Comment(0)
C
9

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.

Coaster answered 28/3, 2017 at 11:0 Comment(0)
P
8
- (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");
    }
}
Precautious answered 1/2, 2013 at 10:0 Comment(1)
This method is not called when the value of the scroll view’s pagingEnabled property is YES.Dentiform
H
6

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
                }
            }
        }
    }
}
Howl answered 4/4, 2016 at 17:33 Comment(0)
F
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

Florin answered 6/6, 2013 at 2:59 Comment(0)
M
5

Swift 5

More clean solution with enum for vertical scrolling.

enum ScrollDirection {
    case up, down
}

var scrollDirection: ScrollDirection? {
    let yTranslation = scrollView.panGestureRecognizer.translation(in: scrollView.superview).y

    if yTranslation > 0 {
        return .up
    } else if yTranslation < 0 {
        return .down
    } else {
        return nil
    }
}

Usage

switch scrollDirection {
case .up:   print("up")
case .down: print("down")
default:    print("no scroll")
}
Massachusetts answered 4/2, 2021 at 14:11 Comment(1)
This works very well! Apparently the panGestureRecognizer retains the last value, so one just needs to do something like add a line savedScrollDirection = scrollDirection (where you define var savedScrollDirection : ScrollDirection? = nil as a class global), in these two delegate methods: scrollViewWillEndDragging(), scrollViewDidEndDecelerating()Rodrigorodrigue
B
4
//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

Bifid answered 1/4, 2020 at 19:22 Comment(0)
G
2

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

Gildea answered 28/8, 2013 at 15:59 Comment(0)
M
2

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
  }
Mama answered 25/3, 2016 at 21:55 Comment(0)
N
2

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
    }
}
Noella answered 10/12, 2018 at 15:36 Comment(0)
B
1

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");
    }
}
Barbershop answered 28/8, 2013 at 6:22 Comment(0)
V
1

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;

       }
Vast answered 3/11, 2014 at 21:46 Comment(0)
I
1

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

Incoercible answered 14/11, 2014 at 9:42 Comment(0)
H
1
- (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.

Herophilus answered 30/6, 2015 at 8:56 Comment(0)
E
1

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
    }
}
Eldoree answered 15/7, 2015 at 18:2 Comment(0)
L
1

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.

Lais answered 27/10, 2015 at 2:47 Comment(0)
C
1

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.

Caloric answered 12/10, 2019 at 10:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.