UIGestureRecognizer Tutorial: Getting Started

I’d advise not to use Auto Layout when you are changing the frame or center properties.

This works for me:

  @IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
    guard let view = recognizer.view else { return }
    switch recognizer.state {
    case .Began:
      originalPosition = view.center
    case .Changed:
      let translation = recognizer.translationInView(self.view)
      view.center = CGPoint(x:view.center.x + translation.x,
                            y:view.center.y + translation.y)
      recognizer.setTranslation(.zero, inView: self.view)
    case .Ended:
      
      UIView.animateWithDuration(1,
                                 delay: 0,
                                 options: .CurveEaseOut,
                                 animations: {
                                  view.center = self.originalPosition
        },
                                 completion: nil)
    default: ()
    }
  }
1 Like

Great, this works! Thank you! Can I set constraints programmatically, as I want to have the monkey in the upper left corner, or will there be problems too?

If you’re only doing simple things, then you shouldn’t run into a problem.

You can always set constraints active or not active. You can also add your constraints into an array and set the entire array active or not.

Or instead of changing the center property, you could use the constraint’s constant property. (Although in a short test, I just added auto layout constraints in code, leaving the above method in place and it worked fine.)

Don’t forget to set the view’s translatesAutoresizingMaskIntoConstraints false if you are setting constraints in code.

Thank you! Changing the constraint’s constant worked in different scenarios and was petty straight forward. Mokeyproblem solved :slight_smile:

1 Like

I’m really enjoying this tutorial thus far. I’m using Xcode 7.3 with Swift 2.2 and am running into some problems getting the loadSound function to work. I changed it to use do/try/catch like this:

func loadSound(filename:NSString) -> AVAudioPlayer { let url = NSBundle.mainBundle().URLForResource(filename as String, withExtension: "caf") // Changed this from tutorial to work in Swift 2 do { let player = try AVAudioPlayer(contentsOfURL: url!) player.prepareToPlay() } catch { print("Error loading \(url)") } return player }

The problem is that the line ‘return player’ gets the error “Use of unresolved identifier ‘player’” which I expect is because ‘player’ was created inside the do/try/catch statement. At this point I’m not sure how to resolve this.

@sigraff - Yes, you’re right - by the time you get to return player, player is out of scope.

You need to define player before the do.

Just declare
let player: AVAudioPlayer
before the do, and then set player during the do.

Hi Caroline,
Thank you so much for the tutorial, it really helped me to introduce to UIGestureRecognizer :slight_smile:
I’m trying to make a slight change but I’m not able to resolve it myself :frowning: Maybe you could give me a hint :wink:
Problem: when I move the monkey or banana, it goes over all the buttons and NavigationBar
 the pan gesture has no limits
Solution I tried: I add the UIImageView (monkey/banana) to a UIView called “agregatedView”, where I thought the monkey would move only within the boundaries of this view.
monkey.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
agregatedView.addSubview(monkey)
monkey.userInteractionEnabled = true
let recognizer = UIPanGestureRecognizer(target: self, action:Selector(“handlePan:”))
monkey.addGestureRecognizer(recognizer)

But NO! the monkey is added to agregatedView, and I can move it wherever I want.
Then, I tried to add the gesture to agregatedView itself, but of course, that moves the whole agregatedView.
what should I do in this case? any help would be really appreciated :blush:
Thank you again!!

@zakia - the monkey won’t stay within the boundaries, because you can position subviews outside of the main view. For example, you may want to animate a view from off-screen to on-screen. In that case, you’d give the subview’s frame negative origin coordinates.

In your gesture handler, you can check the frame of the view that you are moving and check that it doesn’t go outside the view.

In the gesture handler, you have this code:

    if let view = recognizer.view {
      let translation = recognizer.translationInView(self.view)
      view.center = CGPoint(x:view.center.x + translation.x,
        y:view.center.y + translation.y)
      recognizer.setTranslation(CGPointZero, inView: self.view)
    ....

where you set the view’s center with the new position. If the view won’t end up in the area that you want to keep it in, you don’t have to set the new center.

Hi Caroline - great tutorial, thank you. In regards to the ‘Gratuitous Deceleration’ section, I wanted to allow the images to be stopped mid-slide by a tap gesture i.e. another pan gesture, but the gesture was only recognised after the image had stopped moving. I included UIViewAnimationOptions.AllowUserInteraction in the animateWithDuration options, which improved sensitivity, but only when the image was moving slowly and not quickly (very bizarre).

I was also struggling with getting the images to stop moving if they hit the edge of the screen during a slide. Any boundaries set such as:

UIView.animateWithDuration(Double(slideFactor * 2),
      delay: 0,
       // 6
      options: [UIViewAnimationOptions.CurveEaseOut, UIViewAnimationOptions.AllowUserInteraction],
      animations: { recognizer.view!.center = finalPoint
                    
          if self.Banana.frame.origin.x >= self.view.frame.width - self.Banana.frame.width {
               self.Banana.frame.origin.x = self.view.frame.width - self.Banana.frame.width  
          }
      },
      completion: nil)


 were affected by finalPoint, where the image slowed down so that it reached the boundary at the end of its slide instead of maintaining its speed and being stopped mid-slide.

I’m sure there are simple solutions, but perhaps you could advise me what to do here. Thanks once again

@timr - I think UIViewAnimationOptions.AllowUserInteraction should work. But not 100% sure.

For the boundaries, you’ll need to take boundary testing out of if recognizer.state == UIGestureRecognizerState.Ended

Also Banana capitalized implies that it is a type (i.e. a class name). The terminology should be self.bananaView.frame

You could also experiment with using the touch methods such as touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)

For some reason .allowUserInteraction only works at the very end of animation. I’m absolutely baffled. I have tried using touchesMoved to intercept it the view, but I shouldn’t really need to. Besides, it just created new problems.

Are you sure that you have .allowUserInteraction outside that checking state conditional? The original code only does everything when state is .Ended.

And have you tried it on a device?

I’ve tried putting UIViewAnimationOptions.AllowUserInteraction lines outside the .Ended if statement if that’s what you mean? But shouldn’t it work inside the statement since .allowUserInteraction is one of the animateWithDuration options? Doesn’t work on device either

@timr - I’ve thought about it at length. I think if you want to do this, don’t use gestures, but use touches began / moved / ended.

Here’s the thing:

The animation should be done, as it is in the tutorial, when the user lets go (state = .Ended).

At this point, the pan gesture is still being used, so .AllowUserInteraction is irrelevant.

To stop the animation, you need some kind of external stopping. For example, this stops the animation (note no fancy stuff, just stopping monkeyView from animating)

  override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    guard let presentationLayer = monkeyView.layer.presentationLayer() else { return }
    monkeyView.frame = presentationLayer.frame
    monkeyView.layer.removeAllAnimations()
  }

But the pan gesture is still in use, so you can’t just keep dragging the monkeyView without letting go the tap hold and starting the pan again.

I thought disabling the pan gesture and re-enabling the gesture might work, but of course if the tap is still down, then the pan gesture won’t fire.

That’s why I think using the touch methods rather than gestures would be more appropriate here.

But if you find a way and get it working with gestures, I’d love to know!

Yes it’s tricky isn’t it.

With what you’ve said and a bit of testing, I’ve decided it comes down to this:

  1. If you use the pan with touchesBegan, touchesMoved, you can slide the view, but not in the same touch you stop the animation with.

  2. If you use touchesBegan, touchesMoved, and touchesEnded alone, animations continue when any touch is released, but the slide animation doesn’t work.

However, this post uses a different approach and finds a solution to this problem.

Thank you again for the great tutorial!

let url = NSBundle.mainBundle().URLForResource(filename as String, withExtension: "caf")

This line returns nil. Why might this be?

I can’t see anything wrong with the line itself, so further investigation is necessary.

Xcode 7.3?
What does filename contain?
Do you have a filename.caf in your project?

Nice Code But Pan gesture is not working


@aiyub - it should be working. Did you go through the tutorial? When did it stop working?

The sample code has a custom tickle gesture implemented, but you can uncomment a return line to implement the pan gesture instead of the tickle gesture.

In case anyone is interested: Xcode 7.3.1 code is at GitHub - alexbepple/MonkeyPinch: Xcode 7.3.1 code for UIGestureRecognizer tutorial https://www.raywenderlich.com/76020

1 Like