- 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: Using Multiple Gesture Recognizers Simultaneously
Recipe 1-3 builds on the ideas presented in Recipe 1-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:shouldRecognize-SimultaneouslyWithGestureRecognizer: 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.
Recipe 1-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 1-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 1-3 demonstrates how you can add the kind of basic touch method used in Recipe 1-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 1-3 Recognizing Gestures in Parallel
@interface DragView : UIImageView <UIGestureRecognizerDelegate> @end @implementation DragView { CGFloat tx; // x translation CGFloat ty; // y translation CGFloat scale; // zoom scale CGFloat theta; // rotation angle } - (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; scale = self.scaleX; theta = self.rotation; } - (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); // Guard against scaling too low, by limiting the scale factor if (self.scale > 0.5f) { self.transform = CGAffineTransformScale(self.transform, scale, scale); } else { self.transform = CGAffineTransformScale(self.transform, 0.5f, 0.5f); } } - (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; } - (instancetype)initWithImage:(UIImage *)image { // Initialize and set as touchable self = [super initWithImage:image]; if (self) { 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 gesture recognizer method takes one argument, another gesture recognizer. This call creates a dependency between the two gesture recognizers. For the first gesture to trigger, the second gesture must fail. If the second gesture is recognized, the first gesture will not be.
iOS 7 introduces new APIs that offer more flexibility in providing runtime failure conditions via gesture recognizer delegates and subclasses. You implement gestureRecognizer:shouldRequireFailureOfGestureRecognizer: and gestureRecognizer:shouldBe-RequiredToFailByGestureRecognizer: in recognizer delegates and shouldRequireFailureOfGestureRecognizer: and shouldBeRequiredToFailByGestureRecognizer: in subclasses.
Each method returns a Boolean result. A positive response requires the failure condition specified by the method to occur for the gesture to succeed. These UIGestureRecognizer delegate methods are called by the recognizer once per recognition attempt and can be set up between recognizers across view hierarchies, while implementations provided in subclasses can define class-wide failure requirements.
In real life, failure requirements typically mean 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 can limit interface slowdowns to where they’re absolutely needed.