49

How can I get the scroll/swipe direction for up/down in a VC?

I want to add a UIScrollView or something else in my VC that can see if the user swipes/scrolls up or down and then hide/show a UIView depending if it was an up/down gesture.

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
user2722667
  • 8,195
  • 14
  • 52
  • 100

13 Answers13

122

If you use an UIScrollView then you can take benefit from the scrollViewDidScroll: function. You need to save the last position (the contentOffset) it have and the update it like in the following way:

// variable to save the last position visited, default to zero
 private var lastContentOffset: CGFloat = 0

 func scrollViewDidScroll(_ scrollView: UIScrollView) {
     if (self.lastContentOffset > scrollView.contentOffset.y) {
         // move up
     }
     else if (self.lastContentOffset < scrollView.contentOffset.y) {
        // move down
     }

     // update the new position acquired
     self.lastContentOffset = scrollView.contentOffset.y
     print(lastContentOffset)
 }

There are other ways of doing it of course this is one of them.

starball
  • 20,030
  • 7
  • 43
  • 238
Victor Sigler
  • 23,243
  • 14
  • 88
  • 105
  • 3
    Thanks it kinda works. But since scroll views has that bounce effect it fires both functions "didScrollUp" and "didScrollDown" back and forth a few times if I scroll all the way to the top or if I do a "pull to refresh" This results in my view toggeling between hidden/shown a few times. So maby I am off adding a swipe gesture to the scrollview/table view? – user2722667 Aug 06 '15 at 14:27
  • Be careful the `UITableView` has a `UIScrollView` inside!! If you have added another one you need to differentiate both – Victor Sigler Aug 06 '15 at 14:30
  • I am not adding another scrollview. I am using "override func scrollViewDidScroll(scrollView: UIScrollView)" But since the scrollview has that bounce effect it will toggle my view to be shown/hidden a few times. So am I better off adding a swipe gesture on my tableview? – user2722667 Aug 06 '15 at 14:44
  • This worked great for me when I wanted to hide a button when scrolling down, but show the button when scrolling up. I solved the issue with bouncing/scrolling above/below scroll limits with the following that I have posted in a new reply below. – gbotha Dec 27 '15 at 16:59
  • You must need to set bounces to false `self.scrollView.bounces = false` otherwise you'll get random event (mixed up and down movements) at the top or bottom location of scrollView. – Moosa Baloch Feb 12 '18 at 10:50
78

Victor's answer is great, but it's quite expensive, as you're always comparing and storing values. If your goal is to identify the scrolling direction instantly without expensive calculation, then try this using Swift:

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview)
    if translation.y > 0 {
        // swipes from top to bottom of screen -> down
    } else {
        // swipes from bottom to top of screen -> up
    }
}

And there you go. Again, if you need to track constantly, use Victors answer, otherwise I prefer this solution.

LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174
GLS
  • 831
  • 6
  • 8
  • 1
    I used this one at first but be aware that it does not work if you trigger the scrolling programmatically, e.g. via `UIPageControl` – laka Jan 23 '20 at 14:39
  • If I started with a downward scroll and change the direction to upwards without lifting my finger it won't detect the change until I pass the initial point at which I started. This is due to translation.y not resetting to zero until I lift my finger. – HAK May 23 '22 at 09:37
  • This won't work for the case where you tap on the status bar to automatically scroll at the top. – HAK May 23 '22 at 11:26
26

I used Victor's answer with a minor improvement. When scrolling past the end or beginning of the scroll, and then getting the bounce back effect. I have added the constraint by calculating scrollView.contentSize.height - scrollView.frame.height and then limiting the scrollView.contentOffset.y range to be greater than 0 or less than scrollView.contentSize.height - scrollView.frame.height, no changes are made when bouncing back.

func scrollViewDidScroll(_ scrollView: UIScrollView) {

    if lastContentOffset > scrollView.contentOffset.y && lastContentOffset < scrollView.contentSize.height - scrollView.frame.height {
        // move up
    } else if lastContentOffset < scrollView.contentOffset.y && scrollView.contentOffset.y > 0 {
        // move down
    }

    // update the new position acquired
    lastContentOffset = scrollView.contentOffset.y
}
Thiha Aung
  • 5,036
  • 8
  • 36
  • 79
gbotha
  • 1,231
  • 17
  • 23
18

For swift4

Implement the scrollViewDidScroll method that detects when you pan gesture beyond a y-axis of 0:

    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if(scrollView.panGestureRecognizer.translation(in: scrollView.superview).y > 0) {
        print("up")
    }
    else {
        print("down")
    }
 }
Hardik Thakkar
  • 15,269
  • 2
  • 94
  • 81
13

I have tried every single response in this thread but none of them could provide a proper solution for a tableView with bounce enabled. So I just used parts of solutions along with some all-time classic boolean flag solution.

1) So, first of all you could use an enum for the scrollDirection:

enum ScrollDirection {
    case up, down
}

2) Set 3 new private vars to help us store lastOffset, scrollDirection and a flag to enable/disable the scroll direction calculation (helps us ignore the bounce effect of tableView) which you will use later:

private var shouldCalculateScrollDirection = false
private var lastContentOffset: CGFloat = 0
private var scrollDirection: ScrollDirection = .up

3) In the scrollViewDidScroll add the following:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    // The current offset
    let offset = scrollView.contentOffset.y

    // Determine the scolling direction
    if lastContentOffset > offset && shouldCalculateScrollDirection {
        scrollDirection = .down
    }
    else if lastContentOffset < offset && shouldCalculateScrollDirection {
        scrollDirection = .up
    }

    // This needs to be in the last line
    lastContentOffset = offset
}

4) If you have not implemented scrollViewDidEndDragging implement it and add these lines of code inside it:

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    guard !decelerate else { return }
    shouldCalculateScrollDirection = false
}

5) If you have not implemented scrollViewWillBeginDecelerating implement it and add this line of code inside it:

func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
    shouldCalculateScrollDirection = false
}

6) Finally, If you have not implemented scrollViewWillBeginDragging implement it and add this line of code inside it:

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {    
    shouldCalculateScrollDirection = true
}

And if you followed all the steps above you are good to go!

You could go to wherever you want to use the direction and simply write:

switch scrollDirection {
case .up:
    // Do something for scollDirection up
case .down:
    // Do something for scollDirection down
}
Petros Mitakos
  • 151
  • 1
  • 5
4
extension UIScrollView {
    enum ScrollDirection {
        case up, down, unknown
    }
    
    var scrollDirection: ScrollDirection {
        guard let superview = superview else { return .unknown }
        return panGestureRecognizer.translation(in: superview).y > 0 ? .down : .up
    }
}
Serj Rubens
  • 621
  • 8
  • 12
2

Swift 2-4 with UISwipeGestureRecognizer

Another option, is use to use the UISwipeGestureRecognizer that will recognize the swipe to requested direction (That will work on all views and not only on UIScrollView

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let upGs = UISwipeGestureRecognizer(target: self, action: #selector(ViewController.handleSwipes(sender:)))
        let downGs = UISwipeGestureRecognizer(target: self, action: #selector(ViewController.handleSwipes(sender:)))
        
        upGs.direction = .up
        downGs.direction = .down
        
        self.view.addGestureRecognizer(upGs)
        self.view.addGestureRecognizer(downGs)
    }

    @objc func handleSwipes(sender:UISwipeGestureRecognizer) {
        
        if (sender.direction == .up) {
            print("Up")
        }
        
        if (sender.direction == .down) {
            print("Down")
        }
    }
}
Community
  • 1
  • 1
Daniel Krom
  • 9,751
  • 3
  • 43
  • 44
  • My problem is that I have an container view with an embedded /Seguetable view. So my swipes doesnt get picked up. Any idea how I still can get them? Will accept soon – user2722667 Aug 06 '15 at 13:54
2

you could do something like this:

fileprivate var lastContentOffset: CGPoint = .zero

func checkScrollDirection(_ scrollView: UIScrollView) -> UIScrollViewDirection {
    return lastContentOffset.y > scrollView.contentOffset.y ? .up : .down
}

and with scrollViewDelegate:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    switch checkScrollDirection(scrollView) {
    case .up:
        // move up
    case .down:
        // move down
    default:
        break
    }

    lastContentOffset = scrollView.contentOffset
}
Felipe Rolvar
  • 101
  • 1
  • 7
2

I've found that this is the simplest and most flexible option (it works for UICollectionView and UITableView as well).

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

    switch velocity {
    case _ where velocity.y < 0:
        // swipes from top to bottom of screen -> down
        trackingDirection = .down
    case _ where velocity.y > 0:
        // swipes from bottom to top of screen -> up
        trackingDirection = .up
    default: trackingDirection = .none
    }
}

Where this doesn't work though, is if there is 0 velocity - in which case you'll have no choice but to use the accepted answer's stored property solution.

brandonscript
  • 68,675
  • 32
  • 163
  • 220
2

Simply add this method to your view controller:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if (scrollView.contentOffset.y < 0) {
        // Move UP - Show Navigation Bar
        self.navigationController?.setNavigationBarHidden(false, animated: true)
    } else if (scrollView.contentOffset.y > 0) {
        // Move DOWN - Hide Navigation Bar
        self.navigationController?.setNavigationBarHidden(true, animated: true)
    }
}
Nick
  • 138,499
  • 22
  • 57
  • 95
Arthur Stepanov
  • 511
  • 7
  • 4
1

For Swift I think the simplest and most powerful is to do like below. It allows you to track when direction changed and react only once when it changed. Plus you can always access the .lastDirection scrolled property if you need to consult that in your code at any other stage.

enum WMScrollDirection {
    case Up, Down, None
}

class WMScrollView: UIScrollView {
    var lastDirection: WMScrollDirection = .None {
        didSet {
            if oldValue != lastDirection {
                // direction has changed, call your func here
            }
        }
    }

    override var contentOffset: CGPoint {
        willSet {
            if contentOffset.y > newValue.y {
                lastDirection = .Down
            }
            else {
                lastDirection = .Up
            }
        }
    }
}

The above assumes you are only tracking up/down scrolling. It is customizable via the enum. You could add/change to .left and .right to track any direction.

I hope this helps someone.

Cheers

Sasho
  • 3,532
  • 1
  • 31
  • 30
1

I made protocol to reuse Scroll Directions.

Declare these enum and protocols.

enum ScrollDirection {
    case up, left, down, right, none
}

protocol ScrollDirectionDetectable {
    associatedtype ScrollViewType: UIScrollView
    var scrollView: ScrollViewType { get }
    var scrollDirection: ScrollDirection { get set }
    var lastContentOffset: CGPoint { get set }
}

extension ScrollDirectionDetectable {
    var scrollView: ScrollViewType {
        return self.scrollView
    }
}

Usage From ViewController

// Set ScrollDirectionDetectable which has UIScrollViewDelegate
class YourViewController: UIViewController, ScrollDirectionDetectable {
    // any types that inherit UIScrollView can be ScrollViewType
    typealias ScrollViewType = UIScrollView
    var lastContentOffset: CGPoint = .zero
    var scrollDirection: ScrollDirection = .none

}
    extension YourViewController {
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            // Update ScrollView direction
            if self.lastContentOffset.x > scrollView.contentOffset.x {
                scrollDirection = .left
            } else if self.lastContentOffset.x > scrollView.contentOffset.x {
                scrollDirection = .right
            }

            if self.lastContentOffset.y > scrollView.contentOffset.y {
                scrollDirection = .up
            } else if self.lastContentOffset.y < scrollView.contentOffset.y {
                scrollDirection = .down
            }
            self.lastContentOffset.x = scrollView.contentOffset.x
            self.lastContentOffset.y = scrollView.contentOffset.y
        }
    }

If you want to use specific direction, just update specific contentOffset that you want.

Changnam Hong
  • 1,669
  • 18
  • 29
  • Should be self.lastContentOffset.x < scrollView.contentOffset.x for the right direction – David Aug 14 '21 at 14:48
1

This is the way I did. It works in almost all cases I have tried.

  1. User scrolls up or down
  2. User changes direction while scrolling
  3. User doesn't drag any more. And scroll view continues scrolling.
    var goingUp: Bool
    let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView).y
    /// `Velocity` is 0 when user is not dragging.
    if (velocity == 0){
        goingUp = scrollView.panGestureRecognizer.translation(in: scrollView).y < 0
    } else {
        goingUp = velocity < 0
    }
Zeynal
  • 102
  • 1
  • 8