iPhone gesture recognition

Jonah Williams ·

I recently needed to support some basic gesture recognition in an iPhone app. As it turns out this is a little bit tricky to do when the touches occur within the same responder chain as a view which already responds to touches (like a UIScrollView or UIWebView which might need to scroll or zoom in addition to handling my custom gestures).

When the iPhone detects a touch it determines the first responder for that event by recursively calling hitTest:withEvent: to descend down the tree of UIResponder objects in the application’s window. The first reponder is then sent the event and can either respond to it or pass it up the responder chain from view to view controller to parent view. (See “Event Delivery” in the iPhone Application Programming Guide.)

I wanted to be able to detect gestures anywhere in the app so I chose to override sendEvent: in UIWindow. This allowed me to intercept touch events before they were sent to the first responder. Basic left/right swipe detection similar to Apple’s “detecting swipe gestures” example is included below.

#define MIN_SWIPE_DISTANCE 100.0f
#define MAX_VERTICAL_SWIPE_DISTANCE 48.0f

#import

@interface GestureWindow : UIWindow {
	UIView *previousTouchView;
	CGPoint previousTouchLocation;
	NSNotificationCenter *notificationCenter;
}

@property(nonatomic, retain) UIView *previousTouchView;
@property(nonatomic, assign) CGPoint previousTouchLocation;
@property(nonatomic, retain) NSNotificationCenter *notificationCenter;

@end

@implementation GestureWindow

@synthesize previousTouchView;
@synthesize previousTouchLocation;
@synthesize notificationCenter;

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

- (void)sendEvent:(UIEvent *)event {
	NSSet *touches = [event allTouches];
	if([touches count] == 1) {
		UITouch *touch = [touches anyObject];
		CGPoint location = [touch locationInView:touch.view];
		if(touch.phase == UITouchPhaseBegan) {
			//save new single touch
			self.previousTouchView = touch.view;
			self.previousTouchLocation = [touch locationInView:touch.view];
		}
		else if (touch.phase == UITouchPhaseEnded) {
			if([touch.view isEqual:self.previousTouchView] && fabsf(self.previousTouchLocation.y - location.y) <= MAX_VERTICAL_SWIPE_DISTANCE) {
				//evaluate gesture
				if(self.previousTouchLocation.x  MIN_SWIPE_DISTANCE) {
					//left to right
					[self.notificationCenter postNotificationName:@"swipe right" object:self userInfo:[NSDictionary dictionaryWithObject:touch.view forKey:@"view"]];
				}
				else if (self.previousTouchLocation.x > location.x && self.previousTouchLocation.x - location.x > MIN_SWIPE_DISTANCE) {
					//right to left
					[self.notificationCenter postNotificationName:@"swipe left" object:self userInfo:[NSDictionary dictionaryWithObject:touch.view forKey:@"view"]];
				}
			}
			self.previousTouchView = nil;
		}
		else if (touch.phase == UITouchPhaseMoved) {
			if([touch.view isEqual:self.previousTouchView] == NO || fabsf(self.previousTouchLocation.y - location.y) > MAX_VERTICAL_SWIPE_DISTANCE) {
				self.previousTouchView = nil;
			}
		}
		else if (touch.phase == UITouchPhaseCancelled) {
			self.previousTouchView = nil;
		}
	}
	else {
		self.previousTouchView = nil;
	}
	[super sendEvent:event];
}

@end

Unlike Apple’s example, I track the original touch in the gesture rather than just comparing the last two touches. This way a slow swipe where the individual touch events may be close together will still trigger a swipe an a slight reverse of direction at the end of the touch will not unexpectedly reverse the direction of the swipe. Depending on the needs of the app and the type of gesture it needs to support it might also be worth also considering the velocity or acceleration between touches, the overall shape of the touch path, or other factors.