- 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 Multi-Touch Interaction
Enabling Multi-Touch interaction in UIView instances lets iOS recover and respond to more than one finger touch at a time. Set the UIView property multipleTouchEnabled to YES or override isMultipleTouchEnabled for your view. When enabled, each touch callback returns an entire set of touches. When that set’s count exceeds 1, you know you’re dealing with Multi-Touch.
In theory, iOS supports an arbitrary number of touches. You can explore that limit by running the following recipe on an iPad, using as many fingers as possible at once. The practical upper limit has changed over time; this recipe modestly demurs from offering a specific number.
When Multi-Touch was first explored on the iPhone, developers did not dream of the freedom and flexibility that Multi-Touch combined with multiple users offered. Adding Multi-Touch to your games and other applications opens up not just expanded gestures but also new ways of creating profoundly exciting multiuser experiences, especially on larger screens like the iPad. I encourage you to include Multi-Touch support in your applications wherever it is practical and meaningful.
Multi-Touch touches are not grouped. If you touch the screen with two fingers from each hand, for example, there’s no way to determine which touches belong to which hand. The touch order is also arbitrary. Although grouped touches retain the same finger order (or, more specifically, the same memory address) for the lifetime of a single touch event, from touch down through movement to release, the correspondence between touches and fingers may and likely will change the next time your user touches the screen. When you need to distinguish touches from each other, build a touch dictionary indexed by the touch objects, as shown in this recipe.
Perhaps it’s a comfort to know that if you need it, the extra finger support has been built in. Unfortunately, when you are using three or more touches at a time, the screen has a pronounced tendency to lose track of one or more of those fingers. It’s hard to programmatically track smooth gestures when you go beyond two finger touches. So instead of focusing on gesture interpretation, think of the Multi-Touch experience more as a series of time-limited independent interactions. You can treat each touch as a distinct item and process it independently of its fellows.
Recipe 1-9 adds Multi-Touch to a UIView by setting its multipleTouchEnabled property and tracing the lines that each finger draws. It does this by keeping track of each touch’s physical address in memory but without pointing to or retaining the touch per Apple’s recommendations.
This is, obviously, an oddball approach, but it has worked reliably throughout the history of the SDK. That’s because each UITouch object persists at a single address throughout the touch-move-release life cycle. Apple recommends against retaining UITouch instances, which is why the integer values of these objects are used as keys in this recipe. By using the physical address as a key, you can distinguish each touch, even as new touches are added or old touches are removed from the screen.
Be aware that new touches can start their life cycle via touchesBegan:withEvent: independently of others as they move, end, or cancel. Your code should reflect that reality.
This recipe expands from Recipe 1-7. Each touch grows a separate Bezier path, which is painted in the view’s drawRect method. Recipe 1-7 essentially started a new drawing at the end of each touch cycle. That worked well for application bookkeeping but failed when it came to creating a standard drawing application, where you expect to iteratively add elements to a picture.
Recipe 1-9 continues adding traces into a composite picture without erasing old items. Touches collect into an ever-growing mutable array, which can be cleared on user demand. This recipe draws in-progress tracing in a slightly lighter color, to distinguish it from paths that have already been stored to the drawing’s stroke array.
Recipe 1-9. Accumulating User Tracings for a Composite Drawing
@interface TouchTrackerView : UIView { NSMutableArray *strokes; NSMutableDictionary *touchPaths; } - (void) clear; @end @implementation TouchTrackerView // Establish new views with storage initialized for drawing - (id) initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.multipleTouchEnabled = YES; strokes = [NSMutableArray array]; touchPaths = [NSMutableDictionary dictionary]; } return self; } // On clear remove all existing strokes, but not in-progress drawing - (void) clear { [strokes removeAllObjects]; [self setNeedsDisplay]; } // Start touches by adding new paths to the touchPath dictionary - (void) touchesBegan:(NSSet *) touches withEvent:(UIEvent *) event { for (UITouch *touch in touches) { NSString *key = [NSString stringWithFormat:@"%d", (int) touch]; CGPoint pt = [touch locationInView:self]; UIBezierPath *path = [UIBezierPath bezierPath]; path.lineWidth = IS_IPAD? 8: 4; path.lineCapStyle = kCGLineCapRound; [path moveToPoint:pt]; [touchPaths setObject:path forKey:key]; } } // Trace touch movement by growing and stroking the path - (void) touchesMoved:(NSSet *) touches withEvent:(UIEvent *) event { for (UITouch *touch in touches) { NSString *key = [NSString stringWithFormat:@"%d", (int) touch]; UIBezierPath *path = [touchPaths objectForKey:key]; if (!path) break; CGPoint pt = [touch locationInView:self]; [path addLineToPoint:pt]; } [self setNeedsDisplay]; } // On ending a touch, move the path to the strokes array - (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { NSString *key = [NSString stringWithFormat:@"%d", (int) touch]; UIBezierPath *path = [touchPaths objectForKey:key]; if (path) [strokes addObject:path]; [touchPaths removeObjectForKey:key]; } [self setNeedsDisplay]; } - (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesEnded:touches withEvent:event]; } // Draw existing strokes in dark purple, in-progress ones in light - (void) drawRect:(CGRect)rect { [COOKBOOK_PURPLE_COLOR set]; for (UIBezierPath *path in strokes) [path stroke]; [[COOKBOOK_PURPLE_COLOR colorWithAlphaComponent:0.5f] set]; for (UIBezierPath *path in [touchPaths allValues]) [path stroke]; } @end