Unlike simple offsets, affine transforms allow you to meaningfully work with rotation, scaling, and translation all at once. To support transforms, gesture recognizers provide their coordinate changes in absolute terms rather than relative ones. Instead of issuing iterative offset vectors, the UIPanGestureRecognizer returns a single vector representing a translation in terms of some view's coordinate system, typically the coordinate system of the manipulated view's superview. This vector translation lends itself to simple affine transform calculations and can be mathematically combined with other changes to produce a unified transform representing all changes applied simultaneously.
Here's what the handlePan: method looks like using straight transforms and no stored state:
- (void) handlePan: (UIPanGestureRecognizer *) uigr
{
if (uigr.state == UIGestureRecognizerStateEnded)
{
CGPoint newCenter = CGPointMake(
self.center.x + self.transform.tx,
self.center.y + self.transform.ty);
self.center = newCenter;
CGAffineTransform theTransform = self.transform;
theTransform.tx = 0.0f;
theTransform.ty = 0.0f;
self.transform = theTransform;
return;
}
CGPoint translation = [uigr translationInView:self.superview];
CGAffineTransform theTransform = self.transform;
theTransform.tx = translation.x;
theTransform.ty = translation.y;
self.transform = theTransform;
}
Notice how the recognizer checks for the end of interaction, then updates the view's position and resets the transform's translation. This adaptation requires no local storage and would eliminate the need for a touchesBegan:withEvent: method. Without these modifications, Recipe 2 has to store previous state.
Recipe 2: Using a pan gesture recognizer to drag views.
@interface DragView : UIImageView
{
CGPoint previousLocation;
}
@end
@implementation DragView
- (id) initWithImage: (UIImage *) anImage
{
if (self = [super initWithImage:anImage])
{
self.userInteractionEnabled = YES;
UIPanGestureRecognizer *panRecognizer =
[[UIPanGestureRecognizer alloc]
initWithTarget:self action:@selector(handlePan:)];
self.gestureRecognizers = @[panRecognizer];
}
return self;
}
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// Promote the touched view
[self.superview bringSubviewToFront:self];
// Remember original location
previousLocation = self.center;
}
- (void) handlePan: (UIPanGestureRecognizer *) uigr
{
CGPoint translation = [uigr translationInView:self.superview];
self.center = CGPointMake(previousLocation.x + translation.x,
previousLocation.y + translation.y);
}
@end
Using Multiple Gesture Recognizers Simultaneously
Recipe 3 builds off the ideas presented in Recipe 2, but with several differences. First, it introduces multiple recognizers that work in parallel. To achieve this, the code uses three separate recognizers rotation, pinch, and pan and adds them all to the DragView's gestureRecognizers property. It assigns the DragView as the delegate for each recognizer. This allows the DragView to implement the gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: delegate method, enabling these recognizers to work simultaneously. Until this method is added to return YES as its value, only one recognizer will take charge at a time. Using parallel recognizers allows you to, for example, both zoom and rotate in response to a user's pinch gesture.
Note that UITouch objects store an array of gesture recognizers. The items in this array represent each recognizer that receives the touch object in question. When a view is created without gesture recognizers, its responder methods will be passed touches with empty recognizer arrays.
Recipe 3 extends the view's state to include scale and rotation instance variables. These items keep track of previous transformation values and permit the code to build compound affine transforms. These compound transforms, which are established in Recipe 3's updateTransformWithOffset: method, combine translation, rotation, and scaling into a single result. Unlike the previous recipe, this recipe uses transforms uniformly to apply changes to its objects, which is the standard practice for recognizers.
Finally, this recipe introduces a hybrid approach to gesture recognition. Instead of adding a UITapGestureRecognizer to the view's recognizer array, Recipe 3 demonstrates how you can add the kind of basic touch method used in Recipe 1 to catch a triple-tap. In this example, a triple-tap resets the view back to the identity transform. This undoes any manipulation previously applied to the view and reverts it to its original position, orientation, and size. As you can see, the touches began, moved, ended, and cancelled methods work seamlessly alongside the gesture recognizer callbacks, which is the point of including this extra detail in this recipe. Adding a tap recognizer would have worked just as well.
This recipe demonstrates the conciseness of using gesture recognizers to interact with touches.
Recipe 3: Recognizing gestures in parallel.
@interface DragView : UIImageView <UIGestureRecognizerDelegate>
{
CGFloat tx; // x translation
CGFloat ty; // y translation
CGFloat scale; // zoom scale
CGFloat theta; // rotation angle
}
@end
@implementation DragView
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// Promote the touched view
[self.superview bringSubviewToFront:self];
// initialize translation offsets
tx = self.transform.tx;
ty = self.transform.ty;
}
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
if (touch.tapCount == 3)
{
// Reset geometry upon triple-tap
self.transform = CGAffineTransformIdentity;
tx = 0.0f; ty = 0.0f; scale = 1.0f; theta = 0.0f;
}
}
- (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[self touchesEnded:touches withEvent:event];
}
- (void) updateTransformWithOffset: (CGPoint) translation
{
// Create a blended transform representing translation,
// rotation, and scaling
self.transform = CGAffineTransformMakeTranslation(
translation.x + tx, translation.y + ty);
self.transform = CGAffineTransformRotate(self.transform, theta);
self.transform = CGAffineTransformScale(self.transform, scale, scale);
}
- (void) handlePan: (UIPanGestureRecognizer *) uigr
{
CGPoint translation = [uigr translationInView:self.superview];
[self updateTransformWithOffset:translation];
}
- (void) handleRotation: (UIRotationGestureRecognizer *) uigr
{
theta = uigr.rotation;
[self updateTransformWithOffset:CGPointZero];
}
- (void) handlePinch: (UIPinchGestureRecognizer *) uigr
{
scale = uigr.scale;
[self updateTransformWithOffset:CGPointZero];
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
- (id) initWithImage:(UIImage *)image
{
// Initialize and set as touchable
if (!(self = [super initWithImage:image])) return nil;
self.userInteractionEnabled = YES;
// Reset geometry to identities
self.transform = CGAffineTransformIdentity;
tx = 0.0f; ty = 0.0f; scale = 1.0f; theta = 0.0f;
// Add gesture recognizer suite
UIRotationGestureRecognizer *rot = [[UIRotationGestureRecognizer alloc]
initWithTarget:self action:@selector(handleRotation:)];
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc]
initWithTarget:self action:@selector(handlePinch:)];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
initWithTarget:self action:@selector(handlePan:)];
self.gestureRecognizers = @[rot, pinch, pan];
for (UIGestureRecognizer *recognizer in self.gestureRecognizers)
recognizer.delegate = self;
return self;
}
@end
Resolving Gesture Conflicts
Gesture conflicts may arise when you need to recognize several types of gestures at the same time. For example, what happens when you need to recognize both single- and double-taps? Should the single-tap recognizer fire at the first tap, even when the user intends to enter a double-tap? Or should you wait and respond only after it's clear that the user isn't about to add a second tap? The iOS SDK allows you to take these conflicts into account in your code.
Your classes can specify that one gesture must fail in order for another to succeed. Accomplish this by calling requireGestureRecognizerToFail:. This is a gesture method that takes one argument, another gesture recognizer. This call creates a dependency between the object receiving this message and another gesture object. What it means is this: For the first gesture to trigger, the second gesture must fail. If the second gesture is recognized, the first gesture will not be.
In real life, this typically means that the recognizer adds a delay until it can be sure that the dependent recognizer has failed. It waits until the second gesture is no longer possible. Only then does the first recognizer complete. If you recognize both single- and double-taps, the application waits a little longer after the first tap. If no second tap happens, the single-tap fires. Otherwise, the double-tap fires, but not both.
Your GUI responses will slow down to accommodate this change; your single-tap responses become slightly laggy. That's because there's no way to tell if a second tap is coming until time elapses. You should never use both kinds of recognizers where instant responsiveness is critical to your user experience. Try, instead, to design around situations where that tap means "do something now" and avoid requiring both gestures for those modes.
Don't forget that you can add, remove, and disable gesture recognizers on the fly. A single-tap may take your interface to a place where it then makes sense to further distinguish between single- and double-taps. When leaving that mode, you could disable or remove the double-tap recognizer to regain better single-tap recognition. Tweaks like this limit interface slowdowns where needed.
Constraining Movement
One problem with the simple approach of the earlier recipes is that it's entirely possible to drag a view offscreen to the point where the user cannot see or easily recover it. Those recipes use unconstrained movement. There is no check to test whether the object remains in view and is touchable. Recipe 4 fixes this problem by constraining a view's movement to within its parent.
It achieves this by limiting movement in each direction, splitting its checks into separate x and y constraints. This two-check approach allows the view to continue to move even when one direction has passed its maximum. If the view has hit the rightmost edge of its parent, for example, it can still move up and down.


