Bookmark and Share

iPhone SDK Tutorial
Chapter 12. Gestures









12.0 Preview

In this chapter, we'll look at the underlying architecture that lets us detect gesture.
But, what is gesture?
Gesture is any sequence of events that happens from the time we touch the screen with one or more fingers until we lift our fingers off the screen. No matter how long it takes, as long as one or more fingers are still against the screen, we are still within a gesture. A gesture is passed through the system inside an event. Events are generated when we interact with the iPhone's multitouch screen and contain information about the touch or touches that occurred.

Touch refers to a finger being placed on the iPhone's screen. The number of touches involved in a gesture is equal to the number of fingers on the screen at the same time.

A tap happens when we touch the screen with a single finger and the immediately lift our finger off the screen without moving it around. The iPhone keeps track of the number of taps and can tell us if the user double-tapped or multi-tapped. It handles all the timing and other work necessary to differentiate between two single-taps and a double-tap. It's important to note that the iPhone only keeps track of taps when one finger is used. If it detects multiple touches, it resets the tap count to one.

Since gestures get passed through the system inside of events, and events get passed through the responder chain, we should understand how the responder chain works in order to handle gestures properly. The first responder is usually the object with which the user is currently interacting. The first responder is the start of the responder chain. Any class that has UIResponder as one of its superclass is a responder. UIView is a subclass of UIResponder and UIControl is a subclass of UIResponder, so all views and all controls are responders.

If the first responder doesn't handle a particular event like gesture, it passes that event up the responder chain. Normally, when an object responds to an event, that's the end of the line for the event. If the event goes through the entire responder chain and no object handles the event, the event is then discarded.

The first responder is almost always a view or control and get the first shot at responding to an event. If the first responder doesn't handle event, it passes the event to its view controller. If the view controller doesn't consume the event, the event is then passed to the first responder's parent view. If the parent view doesn't respond, the event will go to the parent view's controller. The event will proceed to the view hierarchy, with each view and then that view's controller getting a chance to handle the event. If the event makes it all the way up through the view hierarchy, the event is passed to the application's window. If the window doesn't handle the event, it passes that event to our application's UIApplication object instance.

This process is important. First, it controls the way gestures can be handled. Let's say a user is looking at a table and swipes a finger across a row of that table. What object handles that gesture?

If the swipe is within a view or control that's a subview of the table view cell, that view or control will get a chance to respond. If it doesn't, the table view cell gets a chance. Most table view cells don't respond to gestures, however, and if they don't, the event proceeds up to the table view, then up the rest of the responder chain until something responds to that event or it reaches the end of the line.

Let's take a look at the swipe gesture on the table view cell. If the table's event contains a swipe, then the table view cell takes an action, and the event goes no further. But if the event doesn't contain a swipe gesture, the table view cell is responsible for forwarding that event to the next object in the responder chain.

The code to handle any kind of interaction with the multitouch screen needs to be contained in an object in the responder chain. That means, we can either choose to embed hat code in a subclass of UIView or UIViewController.

  • If the view needs to do something to itself based on the user's touch, the code probably belongs in the class that defines that view.
  • When the gesture being processed affects more that the object being touched, the gesture code belongs in the view's controller class

There are four method used to notify a responder about touches and gestures. When the user first touches the screen, the iPhone looks for a responder that has a method called touchesBegan:withEvent:. To find out when the user first begins a gesture or taps the screen, implement this method in view or view controller. An example of this:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    NSUInteger numTaps = [[touches anyObject] tapCount];
    NSUInteger numTouches = [touches count];
}

All of the touch-related methods, gets passed an NSSet instance called touches and an instance of UIEvent. We can determine the number of fingers currently pressed against the screen by getting a count of the objects in touches. Every object in touches is a UITouch event that represents one finger touching the screen. If this touch is part of series of taps, we can find out the tap count by asking any of the UITouch objects. If there's more than one object in touches, we know the tap count has to be one, because the system keeps tap counts only as long as just one finger is being used to tap the screen. If numTouch is 2, we know the user just double-tapped the screen.

A table view cell doesn't care about touches that are in other rows or that are in the navigation bar. We an get a subset of touches that has only those touches that fall within a particular view from the event:

NSSet *myTouches = [event touchesFor View:self.view];

Every UITouch represents a different finger, and each finger is located at a different position on the screen. We can find out the position of a specific finger using the UITouch object. It will even translate the point into the view's local coordinate system if we ask for it to:

CGPoint point = [touch locationInView:self];

We can be notified while the user is moving fingers across the screen by implementing touchesMoved:withEvent:.

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {

This method is called multiple times during a long drag, and each time it is called, we will get another set of touches and another event. In addition to being able to find out each finger's current position from the UITouch objects, we can also find out the previous location of that touch, which is the finger's position the last time either touchesMoved:withEvent: or touchesBegan:withEvent: was called.

When the user's fingers are removed from the screen, another event, touchesEnded:withEvent:, is invoked. When this method is called, we know that the user is done with a gesture.

Another method, touchesCancelled:withEvent:, is called if the user is in the middle of a gesture when something happens to interrupt it. This is where we can do any cleanup we might need so we can start fresh with a new gesture. When this method is called, touchesEnded:withEvent: will not be called for the current gesture.



12.1 Touch Detecting

In this section, we'll make an application that will help us to understand better related to the four touch responder methods.

Let's create an application using view-based template, and name it TouchExample. We need three labels:

  • To indicate which method was last called.
  • To report the current tap count.
  • To report the number of touches.
TouchExOutput

Here is "TouchExampleViewController.h."

#import <UIKit/UIKit.h>

@interface TouchExampleViewController : UIViewController {
    UILabel    *messageLabel;
    UILabel    *tapsLabel;
    UILabel    *touchesLabel;    
}
@property (nonatomic, retain) IBOutlet UILabel *messageLabel;
@property (nonatomic, retain) IBOutlet UILabel *tapsLabel;
@property (nonatomic, retain) IBOutlet UILabel *touchesLabel;
- (void)updateLabelsFromTouches:(NSSet *)touches;

@end

Save the header file and go to Interface Builder. Make three labels and make connections to the outlets: messageLabel, tapsLabel, and touchesLabel from the top labels in that order. Then open the attribute inspector and check User Interacting Enabled and Multiple Touch.

MultipleTouchCheckBox

Save the nib, and check the file, "TouchExampleViewController.m"


#import "TouchExampleViewController.h"

@implementation TouchExampleViewController
@synthesize messageLabel;
@synthesize tapsLabel;
@synthesize touchesLabel;

- (void)updateLabelsFromTouches:(NSSet *)touches {
    NSUInteger numTaps = [[touches anyObject] tapCount];
    NSString *tapsMessage = [[NSString alloc]
                             initWithFormat:@"%d taps detected", numTaps];
    tapsLabel.text = tapsMessage;
    [tapsMessage release];
    
    NSUInteger numTouches = [touches count];
    NSString *touchMsg = [[NSString alloc] initWithFormat:
                          @"%d touches detected", numTouches];
    touchesLabel.text = touchMsg;
    [touchMsg release];
}

- (void)didReceiveMemoryWarning {
	// Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
	
	// Release any cached data, images, etc that aren't in use.
}

- (void)viewDidUnload {
	// Release any retained subviews of the main view.
	// e.g. self.myOutlet = nil;
    self.messageLabel = nil;
    self.tapsLabel = nil;
    self.touchesLabel = nil;
    [super viewDidUnload];
}

- (void)dealloc {
    [messageLabel release];
    [tapsLabel release];
    [touchesLabel release];
    
    [super dealloc];
}

#pragma mark -

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    messageLabel.text = @"Touches Began";
    [self updateLabelsFromTouches:touches];
    
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{
    messageLabel.text = @"Touches Cancelled";
    [self updateLabelsFromTouches:touches];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    messageLabel.text = @"Touches Stopped.";
    [self updateLabelsFromTouches:touches];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    messageLabel.text = @"Drag Detected";
    [self updateLabelsFromTouches:touches];
    
}
@end

We implemented all four of the touch methods. Each method sets messageLabel to tell the user which method is called. Then, all of them call updateLabelsFromTouches: to update the labels.

Build and Run.
We can simulate two-finger pinch in the iPhone simulator by holding down the option key while dragging the mouse.

TwoFingers

We can also simulate two-finger swipes by first holding down the option key to simulate a pinch, then moving the mouse so the two dots representing virtual fingers are next to each other, and then holding down the shift key while still holding down the option key. Pressing the shift key will lock the position of the two fingers relative to each other, and we can do swipes and other two-finger gestures.

SwipeHorizontal SwipeVertical


12.2 Swipe Detecting

The application we're going to make here does only one thing: detect swipes. It will display a message informing that a swipe was detected.

Detecting swipes is relatively easy. We'll define a minimum gesture length in pixels, which is how far the user has to swipe before the gesture counts as a swipe. We're also going to define a variance, which is how far from a straight line our user can veer and still have the gesture count as a horizontal or vertical swipe. A diagonal line generally won't count as a swipe.

When the user touches the screen, we're going to save the location of the first touch in a variable. hen, we'll check as the user's finger moves across the screen to see if it reaches a point where it has gone far enough and straight enough to count as a swipe.

Let's make a project with view-based template, name it SwipeExample.
Then, look at the file, "SwipeExampleViewController.h."

#import <UIKit/UIKit.h>
#define kMinimumGestureLength       25
#define kMaximumVariance            5

@interface SwipeExampleViewController : UIViewController {
    UILabel    *label;
    CGPoint    gestureStartPoint;    
}
@property (nonatomic, retain) IBOutlet UILabel *label;
@property CGPoint gestureStartPoint;
- (void)eraseText;
@end

We defined a minimum gesture length of 25 pixels and a variance of 5. Then, we declared an outlet for the label and a variable for the spot the user touches and a method to erase the text after a few seconds.

Next, Interface Builder. The view should be set to receive multiple touches. Put a label on the view and delete the label text. Then, set connect the label outlet from the File's Owner icon. Save nib and back to Xcode.

Now, time to look at "SwipeExampleViewController.m."

#import "SwipeExampleViewController.h"

@implementation SwipeExampleViewController
@synthesize label;
@synthesize gestureStartPoint;

- (void)eraseText
{
    label.text = @"";
}

- (void)viewDidUnload {
	// Release any retained subviews of the main view.
	// e.g. self.myOutlet = nil;
    self.label = nil;
}

- (void)dealloc {
    [label release];
    [super dealloc];
}

#pragma mark -
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [touches anyObject];
    gestureStartPoint = [touch locationInView:self.view];
    
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [touches anyObject];
    CGPoint currentPosition = [touch locationInView:self.view];
    
    CGFloat deltaX = fabsf(gestureStartPoint.x - currentPosition.x);
    CGFloat deltaY = fabsf(gestureStartPoint.y - currentPosition.y);
    
    if (deltaX >= kMinimumGestureLength && deltaY <= kMaximumVariance) {
        label.text = @"Horizontal swipe detected";
        [self performSelector:@selector(eraseText)
                   withObject:nil afterDelay:2];
    }
    else if (deltaY >= kMinimumGestureLength &&
             deltaX <= kMaximumVariance){
        label.text = @"Vertical swipe detected";
        [self performSelector:@selector(eraseText) withObject:nil
                   afterDelay:2];
    }
}
@end

The method, touchesBegan:withEvent:, is grabbing any touch from the touches set and store its point. We do not care the number of touches in the example, we just grab one of them.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [touches anyObject];
    gestureStartPoint = [touch locationInView:self.view];
    
}

The method, touchesMoved:withEvent:, gets the current position of the user's finger:

    UITouch *touch = [touches anyObject];
    CGPoint currentPosition = [touch locationInView:self.view];

Then, we calculate how fat the user's finger has moved from its starting position. Once we have the two deltas, we check to see if the user has moved far enough in one direction without having moved too far in the other to make a swipe. If they have, we set the label's text to indicate whether the swipe was detected.
We are using performSelector:withObject:afterDelay: to erase the text after it's been on the screen for 2 seconds.

        [self performSelector:@selector(eraseText) withObject:nil
                   afterDelay:2];

That way, the user can practice multiple swipes without having to worry if the label is referring to an earlier attempt or the most recent display.

Build and Run.



12.3 Detecting Multi Swipes

If we want to implement two- or three-fingers swipes, the application we made in the previous section will have a problem. That's because we use touches as an NSSet, not an NSArray, and sets are unordered. So, we do not know which finger is which. We need to find a way to detect a multiple -finger swipe without falsely identifying other gestures, such as pinches, as swipes. The solution is fairly straightforward.
When touchesBegan:withEvent: gets notified that a gesture has begun, we save one finger;s position just as we did before. No need to save all the finger positions.

When we check for swipes, we loop through all the touches provided to the touchesMoved:withEvent; method, comparing each one to the saved point. If the user did a multiple finger swipe, when comparing to the saved point, at least one of the touches we get in that method will indicate a swipe.



12.4 Detecting Multiple Taps

Though it is easy to detect multiple taps, it's not quite as straightforward if we want to take different actions based on the number of taps. if the user triple-taps, we will be notified three separate times. We get a single-tap, a double-tap, and finally a triple-tap. If we want to do something on a double-tap but something totally different on a triple-tap, having three separate notification could cause a problem.

Our first implementation of multiple tap is going to have four labels, one each that inform us when it has detected a single-tap, double-tap, triple-tap, and quadruple-tap. In this version of the application, all four fields will work independently. So, if we tap four times, we'll be notified of all for tap types.

Once we get the first version working, we'll see how to change its behavior so that only one label appears when the user stops tapping, showing total number of user taps.

The project is using view-based template with the name, MultitapExample. We need outlet for the four labels and separate four methods for each tap scenario. Here is the header file, "MultitapExample.h"

#import <UIKit/UIKit.h>

@interface MultitapExampleViewController : UIViewController {
	UILabel *singleLabel;
	UILabel *doubleLabel;
	UILabel *tripleLabel;
	UILabel *quadrupleLabel;      
}
@property (nonatomic, retain) IBOutlet UILabel *singleLabel;
@property (nonatomic, retain) IBOutlet UILabel *doubleLabel;
@property (nonatomic, retain) IBOutlet UILabel *tripleLabel;
@property (nonatomic, retain) IBOutlet UILabel *quadrupleLabel;
- (void)singleTap;
- (void)doubleTap;
- (void)tripleTap;
- (void)quadrupleTap;
- (void)eraseMe:(UITextField *)textField;
@end

Save the file and go to Interface Builder. Put four labels and connect to appropriate outlet. Save, back to Xcode, and check the file "MultitapExample.m"

#import "MultitapExampleViewController.h"

@implementation MultitapExampleViewController

@synthesize singleLabel;
@synthesize doubleLabel;
@synthesize tripleLabel;
@synthesize quadrupleLabel;

- (void)singleTap {
    singleLabel.text = @"Single Tap Detected";
    [self performSelector:@selector(eraseMe:)
               withObject:singleLabel afterDelay:1.6f];
}

- (void)doubleTap {
    doubleLabel.text = @"Double Tap Detected";
    [self performSelector:@selector(eraseMe:)
               withObject:doubleLabel afterDelay:1.6f];
}

- (void)tripleTap {
    tripleLabel.text = @"Triple Tap Detected";
    [self performSelector:@selector(eraseMe:)
               withObject:tripleLabel afterDelay:1.6f];
}

- (void)quadrupleTap {
    quadrupleLabel.text = @"Quadruple Tap Detected";
    [self performSelector:@selector(eraseMe:)
               withObject:quadrupleLabel afterDelay:1.6f];
}

- (void)eraseMe:(UITextField *)textField {
    textField.text = @"";
}

- (void)viewDidUnload {
	// Release any retained subviews of the main view.
	// e.g. self.myOutlet = nil;
    self.singleLabel = nil;
    self.doubleLabel = nil;
    self.tripleLabel = nil;
    self.quadrupleLabel = nil;
    [super viewDidUnload];
}

- (void)dealloc {
    [singleLabel release];
    [doubleLabel release];
    [tripleLabel release];
    [quadrupleLabel release];    
    [super dealloc];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    NSUInteger tapCount = [touch tapCount];
    switch (tapCount) {
        case 1:
            [self singleTap];
            break;
        case 2:
            [self doubleTap];
            break;
        case 3:
            [self tripleTap];
            break;
        case 4:
            [self quadrupleTap];
            break;
        default:
            break;
    }
}
@end

Build and Run.

The picture below shows the case when we did quadruple-tap.

MultiTapA

But we want to see only one label displayed. So, let's make a new version. In this version, every time we detect a number of taps, instead of calling the corresponding method immediately, we use
performSelector:withObject:afterDelay: to call it 0.4 second in the future, and we cancel the perform request done by our method when the previous tap count was received. So, when we receive one tap, we call the singleTap method 0.4 second in the future. When we receive notification of a double-tap, we cancel the call to singleTap and call doubleTap 0.4 second in the future. So, only one of the four method is called for any particular tap sequence.

The new versions is:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    NSUInteger tapCount = [touch tapCount];
    
    switch (tapCount) {
        case 1:
            [self performSelector:@selector(singleTap) 
                       withObject:nil 
                       afterDelay:.4];
            break;
        case 2:
            [NSObject cancelPreviousPerformRequestsWithTarget:self 
                                                     selector:@selector(singleTap) 
                                                       object:nil];
            [self performSelector:@selector(doubleTap) 
                       withObject:nil 
                       afterDelay:.4];
            break;
        case 3:
            [NSObject cancelPreviousPerformRequestsWithTarget:self 
                                                     selector:@selector(doubleTap) 
                                                       object:nil];
            [self performSelector:@selector(tripleTap) 
                       withObject:nil 
                       afterDelay:.4];
            break;
        case 4:
            [NSObject cancelPreviousPerformRequestsWithTarget:self 
                                                     selector:@selector(tripleTap) 
                                                       object:nil];
            [self quadrupleTap];
            break;
        default:
            break;
    }
}
MultiTapB


12.5 Detecting Pinches

Pinch is used to zoom in if you pinch apart or zoom out if you pinch together. Detecting pinches is pretty easy. When the gesture begins, we check to make sure there are two touches,because pinches are two-finger gestures. If there are two, we store the distance between them. Then, as the gesture progresses, we keep checking the distance between the user's fingers, and if the distance increases or decreases more than a certain amount, we know there's been a pinch.

Make a new project with view-based template and name it PinchExample. We need an outlet for a label and a variable for the starting distance between the fingers, and a method to erase the label.

Here is "PinchExampleViewController.h"

#import <UIKit/UIKit.h>
#define kMinimumPinchDelta 100
@interface PinchExampleViewController : UIViewController {
    UILabel *label;
    CGFloat initialDistance;
}
@property (nonatomic, retain) IBOutlet UILabel *label;
@property CGFloat initialDistance;
- (void)eraseLabel;
@end
 

Go to Interface Builder. Make the view to accept multiple touches and put a single label, and make a connection to the outlet. Then, save nib and back to Xcode.

"PinchExampleViewController.m"

#import "PinchExampleViewController.h"
#import "CGPointUtils.h"

@implementation PinchExampleViewController
@synthesize label;
@synthesize initialDistance;

- (void)eraseLabel {
    label.text = @"";
}

- (void)viewDidUnload {
	// Release any retained subviews of the main view.
	// e.g. self.myOutlet = nil;
    self.label = nil;
}

- (void)dealloc {
    [label release];
    [super dealloc];
}

#pragma mark -
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if ([touches count] == 2) {
        NSArray *twoTouches = [touches allObjects];
        UITouch *first = [twoTouches objectAtIndex:0];
        UITouch *second = [twoTouches objectAtIndex:1];
        initialDistance = distanceBetweenPoints(
            [first locationInView:self.view], 
            [second locationInView:self.view]);
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    
    if ([touches count] == 2) {
        NSArray *twoTouches = [touches allObjects];
        UITouch *first = [twoTouches objectAtIndex:0];
        UITouch *second = [twoTouches objectAtIndex:1];
        CGFloat currentDistance = distanceBetweenPoints(
            [first locationInView:self.view],
            [second locationInView:self.view]);
        
        if (initialDistance == 0)
            initialDistance = currentDistance; 
        else if (currentDistance - initialDistance > kMinimumPinchDelta) {
            label.text = @"Zoom In Pinch";
            [self performSelector:@selector(eraseLabel) 
                    withObject:nil 
                    afterDelay:1.6f];
        }
        else if (initialDistance - currentDistance > kMinimumPinchDelta) {
            label.text = @"Zoom Out Pinch";
            [self performSelector:@selector(eraseLabel) 
                    withObject:nil 
                    afterDelay:1.6f];
        }
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    initialDistance = 0;
}
@end 
 

In the method, touchesBegan:withEvent:, we check to see if this touch involves two fingers. If there are, we figure out the distance between the two points using a method from the CGPointUtils.c and store the result in the instance variable initialDistance.

In touchesMoved:withEvent:, if we have two touches, we calculate the distance between the two touches.

     if ([touches count] == 2) {
        NSArray *twoTouches = [touches allObjects];
        UITouch *first = [twoTouches objectAtIndex:0];
        UITouch *second = [twoTouches objectAtIndex:1];
        CGFloat currentDistance = distanceBetweenPoints(
            [first locationInView:self.view],
            [second locationInView:self.view]);

Then, we check to see if initialDistance is 0. If it's 0, this is the first point where both fingers are against the screen, and we store the current distance between the points as the initial distance.

Otherwise, we check to see if the initial distance subtracted from the current distance is more than the amount we've defined as the minimum change needed to count as a pinch. If so, we have an outward pinch.

        if (initialDistance == 0)
            initialDistance = currentDistance; 
        else if (currentDistance - initialDistance > kMinimumPinchDelta) {
            label.text = @"Zoom In Pinch";
            [self performSelector:@selector(eraseLabel) 
                    withObject:nil 
                    afterDelay:1.6f];

If not, we do another check for an inward pinch by looking to see if initial distance minus the current distance is enough to qualify as a pinch.

        else if (initialDistance - currentDistance > kMinimumPinchDelta) {
            label.text = @"Zoom Out Pinch";
            [self performSelector:@selector(eraseLabel) 
                    withObject:nil 
                    afterDelay:1.6f];
        }

Build and Run. If we're on the simulator, we can simulate a punch by holding down the option key and clicking and dragging in the simulator window using the mouse.

PinchZoomOut