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.
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.
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.
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.
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
}
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")
}
}
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
}
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
}
}
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")
}
}
}
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
}
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.
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)
}
}
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
I made protocol to reuse Scroll Directions.
Declare these enum
and protocol
s.
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.
This is the way I did. It works in almost all cases I have tried.
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 }