iPhone gesture recognition

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.

This entry was posted in Mobile and tagged , . Bookmark the permalink.
  • Steven MCDonald

    Hi there. Great routine there. I am kind of new to programming using SDK etc.
    Can you show me in a few lines of code how I would go about actually detecting the swipe right that you pick up in your routine, inside my actual main program (view controller in my case).

    what is the syntax of responding to the notification detected below in your code?

    I hope I make myself clear.. How do I repsond or pickup this “event detection” in my main code? Thanks.

    //left to right
    [self.notificationCenter postNotificationName:@@"swipe right" object:self userInfo:[NSDictionary dictionaryWithObject:touch.view forKey:@"view"]]

  • Jonah

    Steven, you’ll need to register an object to receive those notifications by adding it as an observer of the notification center. You’ll be able to define a method which is called when a particular notification is posted to that notification center, optionally a notification from a particular source. Take a look at Apple’s notification documentation: http://developer.apple.com/iphone/library/documentation/Cocoa/Conceptual/Notifications/Articles/Notifications.html

  • http://bertmcdowell.com Bert

    Hi There, Great little article.

    I integrated it easily into my project, though I did simplified it by using the window as the source for the touch locations. It’s derived off the UIView interface and I don’t really care which view we are in contact with, I also used the default notification center.

    The source for the implementation is below if you want it.

    Thanks,
    Bert

    const NSInteger MIN_SWIPE_DISTANCE = 100;
    const NSInteger MAX_VERTICAL_SWIPE_DISTANCE = 48;

    @implementation CustomWindow

    // ———————————–
    // Capture the event
    // ———————————–
    - (void)sendEvent:(UIEvent *)event
    {
    NSSet *touches = [event allTouches];
    if([touches count] == 1)
    {
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self];
    if(touch.phase == UITouchPhaseBegan)
    {
    //save new single touch
    touchStart = location;
    }
    else if (touch.phase == UITouchPhaseEnded)
    {
    if(fabsf(touchStart.y – location.y) <= MAX_VERTICAL_SWIPE_DISTANCE)
    {
    //evaluate gesture
    if(touchStart.x MIN_SWIPE_DISTANCE)
    {
    NSLog( @”Swipe Right” );

    //left to right
    [[NSNotificationCenter defaultCenter] postNotificationName:@”swipe right” object:self userInfo:[NSDictionary dictionaryWithObject:self forKey:@"view"]];
    }
    else if (touchStart.x > location.x && touchStart.x – location.x > MIN_SWIPE_DISTANCE)
    {
    NSLog( @”Swipe Left” );

    //right to left
    [[NSNotificationCenter defaultCenter] postNotificationName:@”swipe left” object:self userInfo:[NSDictionary dictionaryWithObject:self forKey:@"view"]];
    }
    }
    }
    }
    [super sendEvent:event];
    }

    @end