Choosing Gestures for an App
Gestures play a role in an iOS app similar to that of game mechanics — they are the primary contact your user has with your app, and it is important to get them right. Choose something cutting edge, and your users may just not get it. Choose to use just the old, basic ones, and your app will probably not be taking full advantage of the iOS environment. Take a standard gesture and give it a different meaning than most apps, and you will most definitely confuse the user.
Your gestures should:
- Feel natural to your user in the context of your app’s primary purpose (it does have a single, overriding purpose, does it not?)
- Not violate standard and expected usage if it is a gesture commonly used across apps
- Be consistent with other gestures used within your app
- Be as simple as possible and still accomplish their intended goal
I am implementing a simple, continuous photo stream and wanted a set of gestures to easily manage the flow. I needed gestures to manipulate the stream by:
- Starting it moving,
- Speeding it up,
- Slowing it down,
- Reversing its direction,
- Temporarily pausing it,
- Manually moving the stream back and forth while paused.
I decided to use standard swiping gestures to control the direction and speed of the photo stream, and a custom continuous gesture recognizer to implement the pause and manual movements.
The Joy of Gesture Recognizers
Gesture recognizers were introduced in iOS 3.2 for use with the iPad, and later in iOS 4.0 for the iPhone and iPod Touch. If all you need are the built-in gestures (there are six), then you may not have to deal with touch events directly at all.
I am using two instances of UISwipeGestureRecognizer to handle the swipe left and swipe right gestures to control the main flow of the photo stream, but none of the other standard recognizers worked for my pause and move gesture. UIPanGestureRecognizer only takes effect once movement is detected, and I need the pause to be effective on first touch. UILongPressGestureRecognizer handles the pause correctly but fails once movement is detected. The gesture processing allows me to customize how recognizers interact with each other, but not in a way I needed. Fortunately, it is pretty straightforward to add a custom recognizer, so I wrote PauseAndMoveGestureRecognizer.
Multiple Gesture Recognizers
My custom recognizer is a continuous recognizer. That means my app receives events when recognition begins, for each movement, and when the gesture ends. I pause the photo stream when recognition begins (upon first touch), and manually move the stream back and forth depending on the movement events generated while the touch is still active. Upon receiving the gesture end event, I allow the photo stream to continue moving as before.
The swipe gesture recognizers are used to change the speed and direction of the photo stream. For instance, by repeatedly swiping left, you can speed the stream up in that direction. Swiping right would then slow the stream down, and so on.
Conflicts Between Gesture Recognizers
An important issue to consider is what behavior you want when multiple gestures are possible at the same moment. When my photo stream is moving across the screen and I touch the stream, it could either be the beginning of UISwipeGestureRecognizer or PauseAndMoveGestureRecognizer. I initially tried to use a single touch for both types of gestures, but could not find a reliable method to do so because of the interaction between my custom recognizer and the other gestures.
By default, gesture processing will generate multiple gesture events for the same set of touches (assuming they fit the pattern of each.) The simplest example is including both a single tap gesture and a double tap gesture: in order to have a double tap, by definition you will first have a single tap. The default behavior is to send both events so that your app will first receive the single tap event, then the double tap event.
I used a timer ([NSTimer scheduledTimerWithTimeInterval:…]) to set a threshold for the pause, and it worked fine when it was the only recognizer involved, but it did not trigger the “Began” event when the swipe gesture recognizers were also included. I assume I was violating some assumption or restriction of the gesture processing by using my own timer event inside the recognizer. (I ended up doing something slightly different which I like better anyway.)
My next attempt was to call the ‘requireGestureRecognizerToFail’ method on my pauseAndMoveGestureRecognizer instance for each of the swipe gestures. By doing this, when either of the swipe gestures is recognized, the gesture processing will cancel my pauseAndMoveGestureRecognizer. If the gesture processing determines that neither swipe gesture is possible, then processing of my custom recognizer continues.
Unfortunately, even though my processing continued, the timing of the events was not what I wanted.
My Custom Gesture Recognizer: PauseAndMoveGestureRecognizer
What ended up working the best was making the swipe gestures require two touches, and the pause and move gesture a single touch. I think this will be clearer to users and also removes any delays, however slight, in pausing the stream.
As you can see from the code below, there is not too much to creating a custom recognizer. Right now, I can’t envision gestures I would want that would not be possible using the UIGestureRecognizer classes.
PauseAndMoveGestureRecognizer.h
// // PauseAndMoveGestureRecognizer.h // // Created by Douglas Sjoquist on 10/17/10. // Copyright 2010 Sunetos, Inc. All rights reserved. // #import <UIKit/UIGestureRecognizerSubclass.h> @interface PauseAndMoveGestureRecognizer : UIGestureRecognizer { BOOL pauseStarted; NSUInteger numberOfTouchesRequired; } @property(nonatomic) NSUInteger numberOfTouchesRequired; // default is 1. - (void)reset; - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; @end
PauseAndMoveGestureRecognizer.m
// // PauseAndMoveGestureRecognizer.m // BrowsePhotos5 // // Created by Douglas Sjoquist on 10/17/10. // Copyright 2010 Sunetos, Inc. All rights reserved. // #import "PauseAndMoveGestureRecognizer.h" @implementation PauseAndMoveGestureRecognizer @synthesize numberOfTouchesRequired; - (id) initWithTarget:(id)target action:(SEL)action { if (self = [super initWithTarget:target action:action]) { self.numberOfTouchesRequired = 1; } return self; } - (void)reset { [super reset]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; if ([touches count] == self.numberOfTouchesRequired) { self.state = UIGestureRecognizerStateBegan; } else { self.state = UIGestureRecognizerStateFailed; } } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; self.state = UIGestureRecognizerStateChanged; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; self.state = UIGestureRecognizerStateEnded; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesCancelled:touches withEvent:event]; self.state = UIGestureRecognizerStateCancelled; } @end
Update
Gesture recognizers are only doing a few simple things with touch events on your behalf so your view does not need to handle them directly.
When multiple recognizers are in play, the combination can be a little confusing, but the advantage of the recognizers is that each of them is looking for a single pattern of touch events. So although the interaction between recognizers may be complex, the internal behavior of a single recognizer remains relatively simple. As long as the set of touches does not violate what the recognizer expects as part of a gesture, it will remain “Possible”. At some point the recognizer will decide to accept or “Recognize” the gesture, or forget about it and “Fail” (for discrete gestures).
Recognizers still need to decide how to handle complex situations, just like you do with traditional touch event handling. Putting one finger down then another after a slight delay is an example of that–the recognizer will receive multiple ‘touchesBegan’ events with different sets of touches and must decide what to do about it. Well written recognizers will handle it smartly and consistently.
The benefit of using standard recognizers is that it simplifies your code, removing details from the main body of your work so you no longer have to worry about them. Also, by using them, you are simply following the standards for simple gestures that Apple is promoting, so your app will behave more like other apps on the device.
For custom recognizers, there are two benefits. One is that your recognizer only needs to be looking for a single pattern of events, keeping the code tighter and cleaner. Another is that once a recognizer fails or is cancelled, the gesture processing acts as a filter so that no further touch events with that particular set of touches will be sent to your recognizer.
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!