Intercepting pan gestures over a UIScrollView breaks scrolling
Asked Answered
N

4

45

I have a vertically-scrolling UIScrollView. I want to also handle horizontal pans on it, while allowing the default vertical scroll behavior. I've put a transparent UIView over the scroll view, and added a pan gesture recognizer to it. This way I can get the pans just fine, but then the scroll view doesn't receive any gestures.

I've implemented the following UIPanGestureRecognizerDelegate methods, hoping to limit my gesture recognizer to horizontal pans only, but that didn't help:

- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer {
    // Only accept horizontal pans here.
    // Leave the vertical pans for scrolling the content.
    CGPoint translation = [gestureRecognizer translationInView:self.view];
    BOOL isHorizontalPan = (fabsf(translation.x) > fabsf(translation.y));
    return  isHorizontalPan;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return (otherGestureRecognizer == _scrollView.panGestureRecognizer);
}
Neville answered 6/12, 2012 at 3:17 Comment(2)
Have you tried [panGesture setCancelsTouchesInView:NO];? Simply returning YES in shouldRecognizeSimultaneouslyWithGestureRecognizer might help you figure out where the problem is as well.Broyles
Just tried it, no difference.Neville
N
54

OK, I figured it out. I needed to do 2 things to make this work:

1) Attach my own pan recognizer to the scroll view itself, not to another view on top of it.

2) This UIGestureRecognizerDelegate method prevents the goofy behavior that happens when both the default scrollview and my own one are invoked simultaneously.

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;
}
Neville answered 6/12, 2012 at 3:46 Comment(2)
the default behavior of shouldRecognizeSimultaneouslyWithGestureRecognizer is to return NO, so that add is unneededNatality
change the return value to YES and it works great.Approximation
E
19

I had the same problem to solve and I did this:

1) Attach my own pan recognizer to the scroll view.

2) Return YES on: – gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:

This will allow both gestures to work. So what that means is that on vertical scroll, both your panGesture delegate and scrollView Delegate will be fired. If it is a horizontal scroll, it will only call your panGesture delegate.

3) in my panGesture delegate, detect if it is a horizontal scroll, if it is not, ignore.

Elocution answered 12/10, 2013 at 21:12 Comment(3)
This is exactly the same that I recommend in my own answer.Neville
@BlackRider it is not. This returns YESFaubert
Thanks gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer did the trick.Swashbuckling
I
11

Swift answer:

let scrollViewPanGesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
scrollViewPanGesture.delegate = self
scrollView.addGestureRecognizer(scrollViewPanGesture)

extension ViewController: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}
Iives answered 8/3, 2018 at 0:21 Comment(2)
Thanks! it works well, but if we are moving the scrollview by the changed location there will be a shaking while we are scrolling the content. Hope there is a solution for that case :)Dishonorable
and can you do me a favor to implement this solution with SWIFTUI?Dishonorable
S
2

For the very anal programmer:

You don't just willy-nilly return true. You should check if the "other" gesture is indeed the scroll view in question.

That's complicated because scrollviews have MANY gesture recognizers attached.

You can do this to see them all, and how many there are in (current) UIKit as you read this

print("count \(primaryScroll.gestureRecognizers?.count)")
print("count \(primaryScroll.gestureRecognizers)") 

So, the full solution is:

import UIKit
class Examp: UIViewController, UIGestureRecognizerDelegate {
    
    var primaryScroll: UIScrollView ... your scroll view in question

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let p = UIPanGestureRecognizer(target: self, action: #selector(panned))
        p.delegate = self
        primaryScroll.addGestureRecognizer(p)
    }
    
    func gestureRecognizer(
     _ g: UIGestureRecognizer,
     shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {

        guard
         let gg = primaryScroll.gestureRecognizers,
         gg.contains(other) else { return false }

        return true
    }

    @objc func panned(p: UIPanGestureRecognizer) {
        let v = p.velocity(in: view).x
        if p.state == .ended && abs(v) > 300 { print("swiped") }
        // etc ...
    }

And that's it.

If you don't check that the other gesture is in fact the scroll view in question, you'll get an incredibly hard-to-debug issue.

Smoke answered 13/3, 2023 at 14:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.