Lecture 6: Gesture Recognizers

Gesture Recognizers

Download the handout code for lecture here.

Download the complete demo app Gestures we made in class here. (Tab bar button item images used in app are from: IconBeast). Many references are made to the app in these notes.

Introduction

From experience, you probably know that there are more than just one way to interact with the touch screen on the iPhone. If you want to zoom into a picture, you "pinch" the screen. When unlocking your phone you "swipe." Maybe in a game you had to "rotate" something. All of these things are called gestures and we will discuss how to incorporate them into your app.

UIGestureRecognizer

All gesture recognizers subclass from the UIGestureRecognizer abstract class. Each subclass is designed to detect and handle a specific gesture. The documentation for UIGestureRecognizer functions and fields is pretty straight forward, so you shouldn't have too much trouble deciphering it. An important function for setting up a gesture recognizer is:

func addTarget(_ target: AnyObject, action action: Selector)

The addTarget function allows you to connect a class to a gesture reconginzer and specify a function that will handle what happens when the gesture is detected. We saw an example of this in the Gesture demo app:

class GamePlayViewController: UIViewController, NewPlayerDelegate {

  var tap = UITapGestureRecognizer()
  ...

  viewDidLoad() {
    self.tap.addTarget(self, action: "tappedView:")

This code is telling the UITapGestureRecognizer to send a message to the GamePlayViewController class when it detects a tap. Then, when the message is received the function tappedView will be called with the sender of the message (the tap gesture recognizer) passed in as an argument of the function (that's what the : after "tappedView" is for).

Another important function for setting up gesture recognizers is:

func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer)

First note that this is not part of the UIGestureRecognizer class, but rather a function of the UIView class. Gesture recognizers help dectect gestures on views and then send messages to other objects to handle the event. They act as some type of middle man/manager in the whole process of detecting a gesture and doing something to respond to it. So, the function addGestureRecognizer allows you to add a UIGestureRecognizer to help detect and handle what happens when a gesture is performed on a particular view. In the Gesture demo app we saw:

class GamePlayViewController: UIViewController, NewPlayerDelegate {

  @IBOutlet var gameBox: UIView!
  
  var tap = UITapGestureRecognizer()
  ...

  viewDidLoad() {
    ...

    self.gameBox.addGestureRecognizer(tap)

Recall that self.gameBox is the UIView that we performed gestures on in our game. In the code we are adding the UITapGestureRecognizer to the gameBox view. What this does is add a detector/handler for when the gameBox view specifically is tapped. Note that a single gesture recognizer can be added to multiple views if you would like the same thing to happen when gestures are performed on different views. However, most of the time this isn't the case, so you can create new gesture recognizers and customize them specifically for the view to which you plan on adding them.

One more thing to note about gesture recognizers is that they have states. The UIGestureRecognizer class has an enum with 6 different possible states for a gesture recognizer:

enum UIGestureRecognizerState : Int {
  case Possible
  case Began
  case Changed
  case Ended
  case Cancelled
  case Failed
}

The three most commonly used states are probably UIGestureRecognizerState.Began, UIGestureRecognizerState.Changed, and UIGestureRecognizerState.Ended. Knowing the state of the gesture recognizer can be helpful if you want to only do something when the gesture begins, changes, ends, etc. For example, in the Gesture demo app we only verified moves after checking that the state of a gesture recognizer was UIGestureRecognizerState.Ended.

So, that's the basic setup for a UIGestureRecognizer. Take a look at the documentation for UIGestureRecognizer to check out other things you can do with any given gesture recognizer (like find the location of where the user performed a gesture on the touch screen, get the number of touches on the screen, etc). Now we will cover the details of each specific gesture recognizer subclass to achieve a better understand of how they work.

UITapGestureRecognizer

The UITapGestureRecognizer detects when the touch screen is tapped. It is probably the most basic and familar gesture.

The fields in the UITapGestureRecognizer are:

var numberOfTapsRequired: Int
var numberOfTouchesRequired: Int

These fields are pretty self explanatory. We used the tap gesture recognizer in the Gesture demo app just by adding it to the gameBox view.

UILongPressGestureRecognizer

The UILongPressGestureRecognizer detects when a user presses down on a view for an extended amount of time (so like holding a press down for one second versus just a quick tap). For example, the app SnapChat uses this gesture. You have to hold down the picture button to record a video and then release when you are finished.

The fields in the UILongPressGestureRecognizer that you can customize are:

var minimumPressDuration: CFTimeInterval
var numberOfTouchesRequired: Int
var numberOfTapsRequired: Int
var allowableMovement: CGFloat
  

These fields are pretty self explanatory.

The minimumPressDuration is the minimum amount of time in seconds that a touch must press on a view before the gesture is recognzied. By default, it is set to 0.5 seconds.

The numberOfTouchesRequired is the number of fingers that must perform a long press in order for the gesture to be recognized. By default, this is set to one.

The numberOfTapsRequired is the number of taps required for the gesture to be recognized. By default this is zero, which makes sense because long press isn't really a tap. But who knows, there could be some good application of using this field (and maybe you could test it out).

The allowableMovement is the maximum a finger can during the gesture before the gesture fails. By default this is set to 10 points.

UIPanGestureRecognizer

The UIPanGestureRecognizer detects when a user drags their finger around on the touch screen. For example, you do this in an app to scroll down a page.

The fields in the UIPanGestureRecognizer that you can customize are:

var maximumNumberOfTouches: Int
var minimumNumberOfTouches: Int

These are pretty self explanatory.

The functions in the UIPanGestureRecognizer class that you can call are:

func translationInView(_ view: UIView) -> CGPoint
func setTranslation(_ translation: CGPoint, inView view: UIView!)
func velocityInView(_ view: UIView!) -> CGPoint

Here's where things start to get more interesting. The long press and tap gestures are both more or less just a tap, which isn't all that exciting. A user doesn't expect too much when they tap or long press. The rest of the gestures have more expectation. When you perform the pan gesture, you expect something to move and drag, right? That is where the above functions come in.

The translationInView takes in a UIView and returns a CGPoint that represents how much the sender's view's center has moved with respect to the UIView passed in. Since the pan gesture comes with the expectation that a view will drag, you can use this function to find where the view moved when the pan gesture was performed. Then, you can apply it by resetting the center of the view. We saw this in the Gesture demo app:

func pannedView(sender: UIPanGestureRecognizer) {
    ...
        var translation = sender.translationInView(self.view)
        var newX = sender.view!.center.x + translation.x
        var newY = sender.view!.center.y + translation.y
        sender.view!.center = CGPointMake(newX, newY)

Since we added the UIPanGestureRecognizer to the gameBox view, we know that sender.view will refer to the gameBox. Therefore, we get the current translation with respect to the GamePlayViewController's view (the gameBox's "superview") from the sender and make the gameBox move by resetting its center.

Next the setTranslation function comes in. Since we already moved the gameBox by resetting its center, we need to reset the translation of the gesture recognizer. This is because the translation of the view from translationInView is not a delta between moves, but rather how much a view has panned over time. We only want to calculate a delta, so if we reset the translation to zero, we basically are doing that. In the Gesture demo app we did it like this:

sender.setTranslation(CGPointZero, inView: self.view)

It's not required to set the translation back to zero though. For example, if you wanted something to pan faster over time, you could set translation to some non-zero CGPoint. You can play around with this and see what happens.

The velocityInView function takes in UIView and returns the velocity in points per second of the sender's view with respect to the view passed in. This might be helpful to the point just mentioned: changing how fast a view pans.

UIPinchGestureRecognizer

The UIPinchGestureRecognizer detects when a user "pinches" the touch screen with two fingers. For the pinch gesture, there is some sort of expectation to zoom in when pinching fingers closer together and to zoom out when fingers are farther apart. For example, you have probably used this gesture to zoom into pictures on an iPhone.

The fields in the UIPinchGestureRecognizer are:

var scale: CGFloat
var velocity: CGFloat { get }

The field scale is a scale factor for the sender's view that is relative to the points of the two pinch touches on the touch screen. It is the scale for the size of the view going from before a pinch to after a pinch. In the Gesture demo app, we used it like this:

func pinchedView(sender: UIPinchGestureRecognizer) {
 ...
          sender.view!.transform = CGAffineTransformScale(sender.view!.transform, sender.scale, sender.scale)
          sender.scale = 1

Every UIView has field called transform that is a CGAffineTransform. Whenever this field is reset, the transform will be applied to the view. Therefore, since we want to scale the view that is being pinched, we reset the sender's view's transform to be scaled. Then, we reset the scale to be 1 since, just like with the pan gesture, the scale isn't a delta, so we need to reset it to in a way make it "act" as a delta between different pinches. Again, this doesn't need to be reset to zero if you want to have something zoom in or zoom out with some sort of acceleration.

The field velocity is the velocity of the pinch in scale factor per second. The field cannot be set because it's read-only, but it could be used to do something interesting with the scale perhaps.

UIRotationGestureRecognizer

The UIRotationGestureRecognizer detects when there are two touches on the touch screen and they are moving in a circular motion. You have probably done this gesture before in a game or maybe when editing pictures on the iPhone.

The fields in the UIRotationGestureRecognizer are:

var rotation: CGFloat
var velocity: CGFloat { get }

The rotation field is the amount the sender's view has rotated in radians. Like other gesture recognizers' fields, it does not measure a delta between rotations, but rather the amount the view has rotated over time. In the Gesture demo app, we used it like this:

func rotatedView(sender: UIRotationGestureRecognizer) {
  ...
  sender.view!.transform = CGAffineTransformMakeRotation(sender.rotation)

As mentioned before, every UIView has field called transform. Here we are creating a rotation transform using the angle from the sender's rotation and applying it to the sender's view.

The field velocity is the velocity of the rotation in radians per second. The field cannot be set because it's read-only, but it could be used to do something interesting setting the rotation.

UISwipeGestureRecognizer

The UISwipeGestureRecognizer detects when a touch swipes across the touch screen. You probably do this all the time to unlock your phone. A lot of games include this gesture as well.

The fields in the UISwipeGestureRecognizer are:

var direction: UISwipeGestureRecognizerDirection
var numberOfTouchesRequired: Int

These fields are pretty self explanatory. The direction field is set using a UISwipeGestureRecognizerDirection. The options for this are:

UISwipeGestureRecognizerDirection.Right
UISwipeGestureRecognizerDirection.Left
UISwipeGestureRecognizerDirection.Up
UISwipeGestureRecognizerDirection.Down

This gesture was not included in the Gesture demo because it was too similar to pan and made the game too difficult. However, it is quite simple to implement since a transformation does not need to by applied to a view. You would set it up in the same way as the other gesture recognizers.