Handling multi-touch in an iPhone application

One of the changes I’m making in the next version of ICanHasCheezburger is improved multi-touch handling. In the current version, I’m only handling left & right swipes to switch to the previous or next images. For the update, I’d like to handle the same gestures as the photo app:

  • Pinch to zoom in & out
  • When zoomed in, drag the image to see different portions
  • When zoomed or moved, double-tap to return to the standard zoom
  • Swipe left & right to move to the next image – but NOT when zoomed in
  • Single tap to toggle toolbar & menu bar

In many cases you can use a UIScrollView, which implements most of those behaviors. For ICHC, I decided to subclass UIImageView to handle touch gestures.

To handle any touch gestures, you need to implement three methods: touchesBegan, touchesMoved, and touchesEnded.

When the user taps in your view, touchesBegan will get called. In that method, you should set up the initial state and determine whether one finger or more than one finger touched the screen.


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	// reset our double-tap timer
    if([timer isValid])
        [timer invalidate];

    moved = NO;
    switch ([touches count]) {
        case 1:
        {
            // handle a single touch
            UITouch * touch = [touches anyObject];
            startTouchPosition = [touch locationInView:self];
            initialDistance = -1;
            break;
        }
        default:
        {
            // handle multi touch
            UITouch *touch1 = [[touches allObjects] objectAtIndex:0];
            UITouch *touch2 = [[touches allObjects] objectAtIndex:1];
            initialDistance = [self distanceBetweenTwoPoints:[touch1 locationInView:self]
                                                     toPoint:[touch2 locationInView:self]];
            break;
        }

    }
}

TouchesMoved will get called continuously as you move your fingers. If we’re doing a ‘pinch’, we track the distance between the fingers and scale the image up or down as the distance changes. If only one finger is down and we’re zoomed in, we drag the image around.


- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch1 = [[touches allObjects] objectAtIndex:0];

    // reset our double tap timer
    if([timer isValid])
        [timer invalidate];

    // if we're zoomed in, move the image
    if (zoomed && ([touches count] == 1)) {
        CGPoint pos = [touch1 locationInView:self];
        self.transform = CGAffineTransformTranslate(self.transform, pos.x - startTouchPosition.x, pos.y - startTouchPosition.y);
        moved = YES;
        return;
    }

    // in a pinch gesture, we scale the image
    if ((initialDistance > 0) && ([touches count] > 1)) {
        UITouch *touch2 = [[touches allObjects] objectAtIndex:1];
        CGFloat currentDistance = [self distanceBetweenTwoPoints:[touch1 locationInView:self]
                                                         toPoint:[touch2 locationInView:self]];
        CGFloat movement = currentDistance - initialDistance;
        NSLog(@"Touch moved: %f", movement);
        if (movement != 0) {
            CGFloat scale = [self scaleAmount: movement];
            self.transform = CGAffineTransformScale(self.transform, scale, scale);
        }
    }
}

Finally, touchesEnded will be called when all fingers are lifted. If only one finger was used, we determine whether it was a tap or swipe based on the distance it moved. If we got a single tap, instead of acting on it immediately we set a timer (I use 0.25 second) and do the single tap action when the timer fires. If we get another tap before that happens, we cancel the timer and do the double tap action. For a pinch gesture, we set the final scaled amount based on the distance between the two touches.


- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch1 = [[touches allObjects] objectAtIndex:0];
    // one finger on the screen
    if ([touches count] == 1) {
        // double tap to reset to default size
        if ([touch1 tapCount] > 1) {
            if (zoomed) {
                self.transform = CGAffineTransformIdentity;
                moved = NO;
                zoomed = NO;
            }
            return;
        }
        CGPoint currentTouchPosition = [touch1 locationInView:self];

        float deltaX = fabsf(startTouchPosition.x - currentTouchPosition.x);
        float deltaY = fabsf(startTouchPosition.y - currentTouchPosition.y);
        // If the swipe tracks correctly.
        if ((deltaX >= HORIZ_SWIPE_DRAG_MIN) && (deltaY < = VERT_SWIPE_DRAG_MAX))
        {
            // It appears to be a swipe.
            if (startTouchPosition.x < currentTouchPosition.x)
            {
                //NSLog(@"Swipe Right");
                [self animateSwipe: 1];
            }
            else
            {
                //NSLog(@"Swipe Left");
                [self animateSwipe: -1];
            }
        }
        else if (!moved && ((deltaX <= TAP_MIN_DRAG) && (deltaY <= TAP_MIN_DRAG)) )
        {
            // Process a tap event.
            NSLog(@"Tap");
            timer = [NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(singleTap:) userInfo:nil repeats:NO];
            [timer retain];
        }
    }
    else {
        // multi-touch
        UITouch *touch2 = [[touches allObjects] objectAtIndex:1];
        CGFloat finalDistance = [self distanceBetweenTwoPoints:[touch1 locationInView:self]
                                                       toPoint:[touch2 locationInView:self]];
        CGFloat movement = finalDistance - initialDistance;
        NSLog(@"Final Distance: %f, movement=%f",finalDistance,movement);
        if (movement != 0) {
            CGFloat scale = [self scaleAmount: movement];
            self.transform = CGAffineTransformScale(self.transform, scale, scale);
            NSLog(@"Scaling: %f", scale);
            zoomed = YES;
        }
   }
}

For a much more in-depth discussion, refer to the excellent multi touch tutorial at iPhone SDK Articles.

You can download my ZoomingImageView class here. It can be used in place of UIImageView to add standard gesture handling. Make sure you enable user interaction on the view.

One response to “Handling multi-touch in an iPhone application

  1. Hi Mike,

    I liked your tutorial, but things like timer I couldn't understand, and I couldn't download the class file that you have linked on your blog. Can you update the links please.

    Thanks!

Leave a Reply