20

I d'like to combine a UILongPressGestureRecognizer with a UIPanGestureRecognizer.

The UIPanGestureRecognizer should start with a long press. Is there a simple way to do this? or do I really have to write my own gesture recognizer?

I wan't something like on the home screen. You press on an icon and after some time the icons start wobbling. Afterwards without releasing my finger from the screen I can start dragging the icon under my finger around.

János
  • 32,867
  • 38
  • 193
  • 353
V1ru8
  • 6,139
  • 4
  • 30
  • 46

7 Answers7

24

actually, you don't have to combine gesture recognizers - you can do this solely with UILongPressGestureRecognizer... You enter StateBegan once your touch(es) have stayed within 'allowableMovement' for 'minimumPressDuration'. You stay in your continuous longPressGesture as long as you don't lift any of your fingers - so you can start moving your fingers and track the movement through StateChanged.

Long-press gestures are continuous. The gesture begins (UIGestureRecognizerStateBegan) when the number of allowable fingers (numberOfTouchesRequired) have been pressed for the specified period (minimumPressDuration) and the touches do not move beyond the allowable range of movement (allowableMovement). The gesture recognizer transitions to the Change state whenever a finger moves, and it ends (UIGestureRecognizerStateEnded) when any of the fingers are lifted.

annie
  • 296
  • 1
  • 4
  • 6
    There a many issues that you can run into though. One of the best things about UIPanGestureRecognizer is that it lets you pan the whole window and still returns values, whereas UILongPressGestureRecognizer doesn't leave subviews (at least those with ClipsToBounds) – Kpmurphy91 Jun 11 '13 at 05:15
  • 4
    Also, no `velocity` is available at when you're moving a LongPress – Scott Zhu Jan 05 '16 at 06:18
21

I had a bit of a hard time for this problem. The accepted answer wasn't enough. No matter what I put in that method the pan or longpress handlers would get invoked. A solution I found was as follows:

  1. Ensure the gesture recognizers' delegates are assigned to the same class (in my case self) and ensure the delegate class is a UIGestureRecognizerDelegate.
  2. Add the following delegate method to your class (as per the answer above):

    - (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer 
    shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { 
         return YES;
    }
    
  3. Add the following delegate method to your class:

    - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
         if([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && ! shouldAllowPan) {
              return NO;
         }
         return YES;
    }
    
  4. Then add either a property or ivar which will track if the pan should be allowed to begin (see method above). In my case BOOL shouldAllowPan.

  5. Set the BOOL to NO in your init or viewDidLoad. Inside your longPress handler set the BOOL to YES. I do it like this:

    - (void) longPressHandler: (UILongPressGestureRecognizer *) gesture {
    
         if(UIGestureRecognizerStateBegan == gesture.state) {
            shouldAllowPan = NO;
         }
    
         if(UIGestureRecognizerStateChanged == gesture.state) {
            shouldAllowPan = YES;
         }
    }
    
  6. Inside the panHandler I do a check on the BOOL:

    - (void)panHandler:(UIPanGestureRecognizer *)sender{
        if(shouldAllowPan) {
              // do your stuff
        }
    
  7. And finally reset the BOOL within the panHandler:

    else if(sender.state == UIGestureRecognizerStateEnded || sender.state == UIGestureRecognizerStateFailed || sender.state == UIGestureRecognizerStateCancelled) {
        shouldAllowPan = NO;
    }
    
  8. And then go grab a beer to congratulate yourself. ;)

Andy B
  • 629
  • 8
  • 15
  • 1
    +1. These steps really helped me to solve op's goal: to only allow drag after long press. Just 2 comments: in step 5 it is enough to check for `Began` and then set to `YES`. ``` if(UIGestureRecognizerStateBegan == gesture.state) { self.shouldAllowDrag = YES; } ``` And in step 6. it's not necessary to check for `shouldAllowPan`. -- sorry for the unformatted code. – Dirk van Oosterbosch Aug 08 '14 at 23:46
  • 1
    I implemented Andy's solution and it worked beautifully. To get the responsiveness I wanted, I also changed the minimum long press duration: UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; lpgr.delegate = self; lpgr.minimumPressDuration = 0.05; – chmaynard Nov 07 '14 at 06:17
  • 1
    This helped me a lot. Thanks! I'll add in a Swift solution so that it can help others. – duyn9uyen May 04 '15 at 14:40
  • Great answer. Thanks! – Matt Long Nov 14 '15 at 04:07
16

I found a solution: This UIGestureRecognizerDelegate method does exactly what I looked for:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer 
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
V1ru8
  • 6,139
  • 4
  • 30
  • 46
11

Andy B's approach in Swift,

  1. Add the UIGestureRecognizerDelegate delegate to the class

    class ViewController: UIViewController, UIGestureRecognizerDelegate
    
  2. Add a member variable

    var shouldAllowPan: Bool = false
    
  3. Add the gestures and need to add the pan gesture delegate to the VC. This is needed to fire off the shouldRecognizeSimultaneouslyWithGestureRecognizer and gestureRecognizerShouldBegin functions

    // long press
    let longPressRec = UILongPressGestureRecognizer(target: self, action: "longPress:")
    yourView.addGestureRecognizer(longPressRec)
    
    // drag
    let panRec = UIPanGestureRecognizer(target: self, action: "draggedView:")
    panRec.delegate = self
    yourView.addGestureRecognizer(panRec)
    
  4. Allow simultaneous gestures

    func gestureRecognizer(UIGestureRecognizer,
    shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
        // println("shouldRecognizeSimultaneouslyWithGestureRecognizer");
        return true
    }
    
    func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
         // We only allow the (drag) gesture to continue if it is within a long press
         if((gestureRecognizer is UIPanGestureRecognizer) && (shouldAllowPan == false)) {
             return false;
         }
         return true;
    }
    
  5. Inside the long press handler:

    func longPress(sender: UILongPressGestureRecognizer) {
    
        if(sender.state == .Began) {
            // handle the long press
        }
        else if(sender.state == .Changed){
            shouldAllowPan = true
    
        }
        else if (sender.state == .Ended) {
            shouldAllowPan = false
        }
    } 
    
duyn9uyen
  • 9,585
  • 12
  • 43
  • 54
  • 4
    You forgot 6. Grab a beer. :) :) – Andy B May 05 '15 at 12:23
  • 1
    @duyn9uyen Thanks for converting it to Swift. – Matt Long Nov 14 '15 at 04:07
  • @duyn9uyen Thanks for conversion and this worked like a charm. – aznelite89 May 19 '17 at 07:48
  • Awesome! Thanks! To enable both gestures (in my project, without the complication of using shouldAllowPan), I just needed to add `UIGestureRecognizerDelegate`, make the `UIPanGestureRecognizer`'s delegate = self, and add the `gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith:UIGestureRecognizer)` function with `return true` – RanLearns Apr 22 '18 at 19:28
  • @RanLearns same here – Sentry.co Oct 22 '18 at 10:47
1

For combinate more gesture :

  1. Create a local variable var shouldAllowSecondGesture : Bool = false
  2. Create the two recognizer

let longPressRec = UILongPressGestureRecognizer(target: self, action: #selector(self.startDrag(sender:))) cell.addGestureRecognizer(longPressRec) let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(sender:))) cell.isUserInteractionEnabled = true cell.addGestureRecognizer(panGestureRecognizer)

  1. Extension your VC and implement GestureRecognizerDelegate for implemented this method.

    extension YourViewController : UIGestureRecognizerDelegate {

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return true
        }
    
    
    
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
             // We only allow the (drag) gesture to continue if it is within a long press
             if((gestureRecognizer is UIPanGestureRecognizer) && (shouldAllowPan == false)) {
                 return false
             }
             return true
        }
    
    
    @objc func startDrag(sender:UIPanGestureRecognizer) {
    
        if(sender.state == .began) {
                // handle the long press
            }
        else if(sender.state == .changed){
                shouldAllowPan = true
    
            }
            else if (sender.state == .ended) {
                shouldAllowPan = false
            }
        }
    
dgalluccio
  • 85
  • 5
0

Read the "Subclassing Notes" section of Apple's UIGestureRecognizer Class Reference at:

https://developer.apple.com/library/prerelease/tvos/documentation/UIKit/Reference/UIGestureRecognizer_Class/

James Bush
  • 1,485
  • 14
  • 19
0

I solved this issue by implementing the desired functionality of the "action: Selector?" func of the UIPanGestureRecognizer within the "action: Selector?" func for the UILongPressGestureRecognizer.

As 'UILongPressGestureRecognizer' has no member 'translation', I calculated the translation by saving the position of the original touch and them extracting it from the actual touch position.


// in target class
var initialTouchX : CGFloat
var initialTouchX : CGFloat


// in the @objc func for the UILongPressGestureRecognizer
if sender.state == .began {
   initialTouchX = sender.location(in: sender.view).x
   initialTouchY = sender.location(in: sender.view).y
}

let translation = CGVector(dx: sender.location(in: sender.view).x - initialTouchX, dy: sender.location(in: sender.view).y - initialTouchY)


Sergey
  • 43
  • 4