27

I have an app that lets the user trace lines on the screen. I am doing so by recording the points within a UIPanGestureRecognizer:

-(void)handlePanFrom:(UIPanGestureRecognizer *)recognizer
{
    CGPoint pixelPos = [recognizer locationInView:rootViewController.glView];
    NSLog(@"recorded point %f,%f",pixelPos.x,pixelPos.y);
}

That works fine. However, I'm very interested in the first point the user tapped before they began panning. But the code above only gives me the points that occurred after the gesture was recognized as a pan (vs. a tap.)

From the documentation, it appears there may be no easy way to determine the initially-tapped location within the UIPanGestureRecognizer API. Although within UIPanGestureRecognizer.h, I found this declaration:

CGPoint _firstScreenLocation;

...which appears to be private, so no luck. I'm considering going outside the UIGestureRecognizer system completely just to capture that initailly-tapped point, and later refer back to it once I know that the user has indeed begun a UIPanGesture. I Thought I would ask here, though, before going down that road.

todd412
  • 1,308
  • 2
  • 17
  • 24
  • The number of omitted points can be much larger than one. The recogniser has to rule out incidental movement, e.g. on touch and hold... So, the slower (less confident/definitive) the touches the more points it will take before getting the first locationInView: from the pan recogniser. For example I recorded the following sequence: touches began at {115, 739}, touches moved at {116, 739}, touches moved at {117, 739}, touches moved at {120, 740}, touches moved at {125, 742}, location in view: {125, 742} – Milos Aug 12 '14 at 14:08
  • 1
    Just to add that combining touchesBegan: and touchesMoved: with a gesture recogniser is not a bad thing: You can start giving the user visual feedback as soon as touchesBegan: but not commit to anything until UIGestureRecognizerStateBegan... – Milos Aug 12 '14 at 14:36

8 Answers8

26

Late to the party, but I notice that nothing above actually answers the question, and there is in fact a way to do this. You must subclass UIPanGestureRecognizer and include:

#import <UIKit/UIGestureRecognizerSubclass.h>

either in the Objective-C file in which you write the class or in your Swift bridging header. This will allow you to override the touchesBegan:withEvent method as follows:

class SomeCoolPanGestureRecognizer: UIPanGestureRecognizer {
    private var initialTouchLocation: CGPoint!

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent) {
        super.touchesBegan(touches, withEvent: event)
        initialTouchLocation = touches.first!.locationInView(view)
    }
}

Then your property initialTouchLocation will contain the information you seek. Of course in my example I make the assumption that the first touch in the set of touches is the one of interest, which makes sense if you have a maximumNumberOfTouches of 1. You may want to use more sophistication in finding the touch of interest.

Edit: Swift 5


import UIKit.UIGestureRecognizerSubclass

class InitialPanGestureRecognizer: UIPanGestureRecognizer {
  private var initialTouchLocation: CGPoint!

  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
    super.touchesBegan(touches, with: event)
    initialTouchLocation = touches.first!.location(in: view)
  }
}
Jeremy Cochoy
  • 2,480
  • 2
  • 24
  • 39
John Lawrence
  • 276
  • 3
  • 2
21

You should be able to use translationInView: to calculate the starting location unless you reset it in between. Get the translation and the current location of touch and use it to find the starting point of the touch.

Warpling
  • 2,024
  • 2
  • 22
  • 38
Deepak Danduprolu
  • 44,595
  • 12
  • 101
  • 105
  • 1
    Unfortunately, translationInView: returns {0, 0} in the state UIGestureRecognizerStateBegan. This means that this approach yields the same end result as when using just locationInView: – Milos Aug 13 '14 at 11:02
  • @milos it is different. `translationInView` will return `{0, 0}` if the gesture wasn't fast enough to caused some translation before it was *recognized*. – Warpling Jul 21 '15 at 22:22
  • See @john-lawrence's answer below for an extensible solution. – Warpling Jul 21 '15 at 22:23
  • Yeah translationInView got me closer to the initial touch, but still not quite correct. I think subclassing UIPanGestureRecognizer is the way to go. – Andrew Rasmussen Jul 20 '16 at 00:37
10

@John Lawrence has it right.

Updated for Swift 3:

import UIKit.UIGestureRecognizerSubclass

class PanRecognizerWithInitialTouch : UIPanGestureRecognizer {
  var initialTouchLocation: CGPoint!
  
  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
    super.touchesBegan(touches, with: event)
    initialTouchLocation = touches.first!.location(in: view)
  }
}

Note that the instance variable initialTouchLocation cannot be private, if you want to access it from your subclass instance (handler).

Now in the handler,

  func handlePan (_ sender: PanRecognizerWithInitialTouch) {
    let pos = sender.location(in: view)
    
    switch (sender.state) {
    case UIGestureRecognizerState.began:
      print("Pan Start at \(sender.initialTouchLocation)")
      
    case UIGestureRecognizerState.changed:
      print("    Move to \(pos)")
Community
  • 1
  • 1
Brent Faust
  • 9,103
  • 6
  • 53
  • 57
5

You could use this method:

CGPoint point    = [gesture locationInView:self.view];
Mickael West
  • 341
  • 4
  • 6
3

I have also noticed that when I attempt to read the value in the shouldBegin method on a UIPanGestureRecognizer, I only see its location after the user moved a little bit (i.e. when the gesture is beginning to recognize a pan). It would be very useful to know where this pan gesture actually started though so that I can decide if it should recognize or not.

If you don't want to subclass UIGestureRecognizer view, you have two options:

  1. UILongPressGestureRecognizer, and set delay to 0
  2. UIPanGestureRecognizer, and capture start point in shouldReceiveTouch

If you have other gestures (e.g. tap, double tap, etc), then you'll likely want option 2 because the long press gesture recognizer with delay of 0 will cause other gestures to not be recognized properly.

If you don't care about other gestures, and only want the pan to work properly, then you could use a UILongPressGestureRecognizer with a 0 delay and it'll be easier to maintain cause you don't need to manually keep track of a start point.


Solution 1: UILongPressGestureRecognizer

Good for: simplicity

Bad for: playing nice with other gesture handlers

When creating the gesture, make sure to set minimumPressDuration to 0. This will ensure that all your delegate methods (e.g. should begin) will receive the first touch properly.

Because a UILongPressGestureRecognizer is a continuous gesture recognizer (as opposed to a discrete gesture recognizer), you can handle movement by handling the UIGestureRecognizer.State.changed property just as you would with a UIPanGestureRecognizer (which is also a continuous gesture recognizer). Essentially you're combining the two gestures.

let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(gestureHandler(_:))
gestureRecognizer.minimumPressDuration = 0

Solution 2: UIPanGestureRecognizer

Good for: Playing nicely with other gesture recognizers

Bad for: Takes a little more effort saving the start state

The steps:

  1. First, you'll need to register as the delegate and listen for the shouldReceiveTouch event.

  2. Once that happens, save the touch point in some variable (not the gesture point!).

  3. When it comes time to decide if you actually want to start the gesture, read this variable.

var gestureStartPoint: CGPoint? = nil

// ...

let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(gestureHandler(_:))
gestureRecognizer.delegate = self

// ...

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
    gestureStartPoint = touch.location(in: self)
    return true
}

Warning: Make sure to read touch.location(in: self) rather than gestureRecognizer.location(in: self) as the former is the only way to get the start position accurately.

You can now use gestureStartPoint anywhere you want, such as should begin:

func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    return isValidStartPoint(gestureStartPoint!)
}
Senseful
  • 86,719
  • 67
  • 308
  • 465
1

in the same UIView put in this method.

//-----------------------------------------------------------------------
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    CGPoint point = [[[event allTouches] anyObject] locationInView:self];
NSLog(@"point.x ,point.y  : %f, %f",point.x ,point.y);
}

look for it in the UIGestureRecognizer Class Reference here: https://developer.apple.com/library/ios/documentation/uikit/reference/UIGestureRecognizer_Class/Reference/Reference.html

hokkuk
  • 675
  • 5
  • 21
-2

You can use UIGestureRecognizerStateBegan method. Here is the link to Apple documentation on UIGestureRecognizer class.

http://developer.apple.com/library/ios/ipad/#documentation/uikit/reference/UIGestureRecognizer_Class/Reference/Reference.html%23//apple_ref/occ/cl/UIGestureRecognizer

user523234
  • 14,323
  • 10
  • 62
  • 102
  • 3
    Unfortunately, that actually isn't the case. I should have been more specific in my initial explanation: I actually use UIGestureRecognizerStateBegan within a case-switch statement to detect the beginning of the pan gesture. But the locationInView detected within that state block is the first point >after< the pan began, not the initial point tapped. – todd412 Aug 06 '11 at 04:29
-3

Wow, A couple years late.. I ran into this problem, but solved it with a static variable:

- (IBAction)handleGesture:(UIPanGestureRecognizer *)recog {

    CGPoint loc = [recognizer locationInView:self.container];
    static CGFloat origin = 0.0f;

    switch (recognizer.state) {
        case UIGestureRecognizerStateBegan:
            origin = loc.x;
            break;
        case UIGestureRecognizerStateChanged:
        case UIGestureRecognizerStatePossible:
            // origin is still set here to the original touch point!
            break;
        case UIGestureRecognizerStateEnded:
            break;
        case UIGestureRecognizerStateFailed:
        case UIGestureRecognizerStateCancelled:
            break;
    }

}

The variable is only set when recognizer state is UIGestureRecognizerBegan. Since the variable is static, the value persists to later method calls.

For my case, I just needed the x coordinate, but you can change it to a CGPoint if you need.

David
  • 3
  • 1
  • It may be counter-intuitive, but this doesn't work, since the initial location passed to the UIPanGestureRecognizer's handler (in the Began state) is incorrect (shifted by 12 pixels or so, and thus the reason for the OP's question). – Brent Faust May 11 '17 at 20:59