Adding subgestures to iOS gesture recognition

I want users of my photo application to feel like they are physically manipulating their old stack of photos. To do that well, it needs the ability to respond to multiple simultaneous touches and movements in a consistent and coherent manner. UIGestureRecognizer provides an interpretive layer that I like, but it is designed around UIEvent and handles touches in sets.

UIGestureRecognizer’s multitouch recognition is elegant, but not exactly what I want. I hope to develop a set of classes for subgesture recognition that follows that style for individual touches, interpreting the user’s intent for each touch independent of other touches. For example, one touch may be interpreted as a pause or hold gesture, while simultaneously another touch is recognized as a single tap, and yet another as a swipe, all without regard to the UIEvent or when other touches began, moved or ended.

Multitouch Events and UIGestureRecognizer

In The Joy of Gesture Recognizers in iOS, I gave a quick overview of UIGestureRecognizer. It is an interpretive layer that takes the raw data of touches, applies some internal rules, and produces a higher level answer when it “recognizes” the user’s intent.

UIGestureRecognizer happens within the context of a single UIEvent. In all iOS touch handling, when the first touch is made, a UIEvent is created. As long as there is at least one touch still active, the UIEvent instance stays the same, and all touch events reference that same instance. Instances of UIGestureRecognizer use that behavior to decide between competing possible gestures. A single UIEvent instance may contain multiple touches but is usually interpreted as a single gesture. (The framework does support other options.)

The set of possible gestures is limitless, but only a subset are very useful. A simple programming exercise to explore multiple touch events can show some interesting possibilities. For instance, write a simple app that merely prints out individual touches and experiment with adding and removing touches within the same event. You will find that you can end up with five completely different touches than those you began with. Since this is all within the same UIEvent, they could all be used to create a very convoluted recognizer (not recommended.)

Generating Subgestures From Individual Touches

If you need to manage touches individually, you can ignore the UIEvent and track each touch yourself. But this means gesture recognition is all on you and the UIGestureRecognizer related classes will not help. The framework I am developing uses subgesture recognizers to interpret the timing and position of individual touches.

The core class is the custom gesture recognizer, IGIndividualSubgestureRecognizer. It’s primary responsibility is to track individual touches and delegate touch events to it’s list of subgesture recognizer (subclasses of IGSubgestureRecognizer) for subgesture recognition. Once an individual touch’s behavior has been recognized by one of the subgesture recognizers, all others will be cancelled. The current version is limited to recognition within a single UIEvent which means something like a single touch, double tap cannot yet be recognized. I want to add the ability to cross the UIEvent boundary in a future version.

IGIndividualSubgestureRecognizer maps each touch event to an instance of IGTouchInfo. These instances track the time and position of each touch event that subgesture recognizers can use to interpret the behavior. IGSubgestureRecognizer instances maintain the current state of each touch in relationship to itself. Once the touch reaches one of the final states for a particular subgesture recognizer, the controlling class IGIndividualSubgestureRecognizer will no longer pass on further events for that touch to that subgesture recognizer.

So if one IGSubgestureRecognizer instance recognized a subgesture for a given touch, then IGIndividualSubgestureRecognizer would set the state of that touch to cancelled for other IGSubgestureRecognizer instances. Once that happens, no instances would receive further events for that touch since one reached a successful final state, and the others reached a cancelled final state.

Subgesture recognizers inherit from IGSubgestureRecognizer. They only need to override the touch event methods and call the setState:forTouch: method when a gesture is recognized or not.

IGSubgestureRecognizer.h

@protocol IGSubgestureRecognizerDelegate<NSObject>
- (void) subgestureRecognizer:(IGSubgestureRecognizer *) subgestureRecognizer
                     forTouch:(IGTouchInfo *) touchInfo
                 changedState:(UIGestureRecognizerState) state;
@end

@interface IGSubgestureRecognizer :NSObject {
    NSMutableDictionary *touchInfoStateMap;
    IGIndependentGestureRecognizer *independentGestureRecognizer;
    id<IGSubgestureRecognizerDelegate> delegate;
}

- (id) initWithIndependentGestureRecognizer:(IGIndependentGestureRecognizer *) aIndependentGestureRecognizer delegate:(id<IGSubgestureRecognizerDelegate>) aDelegate;

- (UIGestureRecognizerState) stateForTouch:(IGTouchInfo *) touchInfo;
- (void) setState:(UIGestureRecognizerState) state forTouch:(IGTouchInfo *) touchInfo;
- (BOOL) isActive:(IGTouchInfo *) touchInfo;

- (void) reset;
- (void) touchBegan:(IGTouchInfo *) touchInfo;
- (void) touchMoved:(IGTouchInfo *) touchInfo;
- (void) touchEnded:(IGTouchInfo *) touchInfo;
- (void) touchCancelled:(IGTouchInfo *) touchInfo;

@end

IGSGRTap is an example of a simple single tap subgesture recognizer.

IGSGRTap.m

@implementation IGSGRTap

static NSTimeInterval maximumTimeDiff = 0.2;
static CGFloat maximumMovementRange = 5.0;

- (void)touchEnded:(IGTouchInfo *) touchInfo {
    CGFloat movementRange = [touchInfo movementRange];
    NSTimeInterval timeDiff = [touchInfo timeDifference];

    // a single tap is defined by the maximum time the touch was down,
    // and the maximum range it moved (allow a little shaking, but no real movement)
    if ((timeDiff < maximumTimeDiff) && (movementRange < maximumMovementRange)){
        [self setState:UIGestureRecognizerStateRecognized forTouch:touchInfo];
    } else {
        [self setState:UIGestureRecognizerStateFailed forTouch:touchInfo];
    }
}

- (void)touchCancelled:(IGTouchInfo *) touchInfo {
    [self setState:UIGestureRecognizerStateCancelled forTouch:touchInfo];
}

@end

IGSubgestures Project Will Be Released to GitHub at Some Point

I have the underlying subgesture recognition working, and will be adding recognizers to match these recognizers available in the UIGestureRecognizer family:

  • Single Tap Gestures
  • Swipe Gestures
  • Pan Gestures
  • Long Press Gestures

Once those are working, I want to add things like flick gestures and single touch spin gestures.

I will be posting the code to github at some point, no promises on exactly when, but I do hope to have a version uploaded before November is over. I will write up a new blog post when I publish the code, so if you are interested, either follow me on twitter, or subscribe to my RSS feed. The license will be something very simple so that you will be able to use it in your own code with no worries.

Please let me know what you think on twitter or in the comments below.


As an indie developer, one of the best things you can do is to find like-minded developers that will provide encouragement and motivation while pursuing a commitment. A great collection of indie iOS developers have helped me stay on track, most of them are either developers associated with iDevBlogADay, or those I have met through the 360iDev conferences. If you can make it to Austin in November, I highly recommend it for its content, the friendships you’ll develop, and the passion it will bring to your iOS development.

Also, here is a little more information about me, Doug Sjoquist, and how I came to my current place in life. You should follow me on twitter and subscribe to my blog. Have a great day!