- Touches
- Recipe: Adding a Simple Direct Manipulation Interface
- Recipe: Adding Pan Gesture Recognizers
- Recipe: Using Multiple Gesture Recognizers Simultaneously
- Recipe: Constraining Movement
- Recipe: Testing Touches
- Recipe: Testing Against a Bitmap
- Recipe: Drawing Touches Onscreen
- Recipe: Smoothing Drawings
- Recipe: Using Multi-Touch Interaction
- Recipe: Detecting Circles
- Recipe: Creating a Custom Gesture Recognizer
- Recipe: Dragging from a Scroll View
- Recipe: Live Touch Feedback
- Recipe: Adding Menus to Views
- Summary
Recipe: Adding Pan Gesture Recognizers
With gesture recognizers, you can achieve the same kind of interaction shown in Recipe 1-1 without working quite so directly with touch handlers. Pan gesture recognizers detect dragging gestures. They allow you to assign a callback that triggers whenever iOS senses panning.
Recipe 1-2 mimics Recipe 1-1’s behavior by adding a recognizer to the view when it is first initialized. As iOS detects the user dragging on a DragView instance, the handlePan: callback updates the view’s center to match the distance dragged.
This code uses what might seem like an odd way of calculating distance. It stores the original view location in an instance variable (previousLocation) and then calculates the offset from that point each time the view updates with a pan detection callback. This allows you to use affine transforms or apply the setTranslation:inView: method; you normally do not move view centers, as done here. This recipe creates a dx/dy offset pair and applies that offset to the view’s center, changing the view’s actual frame.
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, 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 and 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 1-2 has to store the previous state.
Recipe 1-2 Using a Pan Gesture Recognizer to Drag Views
@implementation DragView { CGPoint previousLocation; } - (instancetype)initWithImage:(UIImage *)anImage { self = [super initWithImage:anImage]; if (self) { 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