Zooming by Tapping

While the basic UIScrollView class supports the pinch-in and pinch-out gestures with a very small amount of code, supporting a more rich zooming experience using tap detection requires more work on the part of your application.

The iOS Human Interface Guidelines defines a double-tap to zoom in and zoom out. That, however, assumes some specific constraints: that the view has a single level of zoom, such as in the Photos application, or that successive double-taps will zoom to the maximum amount and, once reached the next double-tap zooms back to the full-screen view. But some applications require a more flexible behavior when dealing with tap-to-zoom functionality, an example of this is the Maps application. Maps supports double-tap to zoom in, with additional double-taps zooming in further. To zoom out in successive amounts, Maps uses a two-finger touch, with the fingers close together, to zoom out in stages. While this gesture is not defined in the iOS Human Interface Guidelines, applications may choose to adopt it to mimic the Maps application, when the functionality is required.

In order for your application to support tap to zoom functionality, you do not need to subclass the UIScrollView class. Instead you implement the required touch handling in the class for which the UIScrollView delegate method viewForZoomingInScrollView: returns. That class will be responsible for tracking the number of fingers on the screen and the tap count. When it detects a single tap, a double tap, or a two-finger touch, it will respond accordingly. In the case of the double tap and two-finger touch, it should programmatically zoom the scroll view by the appropriate factor.

Implementing the Touch-Handling Code

Supporting the tap, double tap, and two-finger tap in the touch code of a subclass of UIView (or a descendent) requires implementing three methods: touchesBegan:withEvent:, touchesEnded:withEvent:, and touchesCanceled:withEvent:. In addition, initialization of interaction, multiple touches, and tracking variables may be required. The following code fragments are from the TapToZoom example in the ScrollViewSuite sample code project, in the TapDetectingImageView class, which is a subclass of UIImageView.

Initialization

The gestures that are desired for the tap-to-zoom implementation require that user interaction and multiple touches are enabled for the view, and the methods to enable that functionality are called from the initWithImage: method. This method also initializes two instance variables that are used to track state in the touch methods. The twoFingerTapIsPossible property is a Boolean that is YES unless more than two fingers are in contact with the device screen. The multipleTouches property has a value of NO unless their are more than one touch events detected. A third property, tapLocation, is a CGPoint that is used to track the location of a double tap or the midpoint between the two fingers when a double touch is detected. This point is then used as the center point for zooming in or out using the programmatic zooming methods described in Zooming Programmatically.

- (id)initWithImage:(UIImage *)image {
    self = [super initWithImage:image];
    if (self) {
        [self setUserInteractionEnabled:YES];
        [self setMultipleTouchEnabled:YES];
        twoFingerTapIsPossible = YES;
        multipleTouches = NO;
    }
    return self;
}

Once the initialization has occurred the class is ready when it receives touch events.

The touchesBegan:withEvent: Implementation

The touchesBegan:withEvent: method first cancels any outstanding attempts to initiate handling a single finger tap, the handleSingleTap message. The message is canceled because, if it has been sent, it is invalid as this is an additional touch event, ruling out a single tap. If the message has not been sent because this is the first touch, canceling the perform is ignored.

The method then updates the state of the tracking variables. If more than a single touch event has been received, then multipleTouches property is set to YES, because this may be a two finger touch. If more than two touch events have occurred, then the twoFingerTapIsPossible property is set to NO, touches by more than two fingers at a time are a gesture that is ignored.

The code for this method is as follows:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // Cancel any pending handleSingleTap messages.
    [NSObject cancelPreviousPerformRequestsWithTarget:self
                                             selector:@selector(handleSingleTap)
                                               object:nil];
 
    // Update the touch state.
    if ([[event touchesForView:self] count] > 1)
        multipleTouches = YES;
    if ([[event touchesForView:self] count] > 2)
        twoFingerTapIsPossible = NO;
 
}

The touchesEnded:withEvent: Implementation

This method is the workhorse of the tap handling and is somewhat complex. However, the code is well documented and is simply shown below. The midPointBetweenPoints function is used to determine the point which a double touch will be centered upon when the handleTwoFingerTap method is called, which results in the view zooming out a level.

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    BOOL allTouchesEnded = ([touches count] == [[event touchesForView:self] count]);
 
    // first check for plain single/double tap, which is only possible if we haven't seen multiple touches
    if (!multipleTouches) {
        UITouch *touch = [touches anyObject];
        tapLocation = [touch locationInView:self];
 
        if ([touch tapCount] == 1) {
            [self performSelector:@selector(handleSingleTap)
                       withObject:nil
                       afterDelay:DOUBLE_TAP_DELAY];
        } else if([touch tapCount] == 2) {
            [self handleDoubleTap];
        }
    }
 
    // Check for a 2-finger tap if there have been multiple touches
    // and haven't that situation has not been ruled out
    else if (multipleTouches && twoFingerTapIsPossible) {
 
        // case 1: this is the end of both touches at once
        if ([touches count] == 2 && allTouchesEnded) {
            int i = 0;
            int tapCounts[2];
            CGPoint tapLocations[2];
            for (UITouch *touch in touches) {
                tapCounts[i] = [touch tapCount];
                tapLocations[i] = [touch locationInView:self];
                i++;
            }
            if (tapCounts[0] == 1 && tapCounts[1] == 1) {
                // it's a two-finger tap if they're both single taps
                tapLocation = midpointBetweenPoints(tapLocations[0],
                                                    tapLocations[1]);
                [self handleTwoFingerTap];
            }
        }
 
        // Case 2: this is the end of one touch, and the other hasn't ended yet
        else if ([touches count] == 1 && !allTouchesEnded) {
            UITouch *touch = [touches anyObject];
            if ([touch tapCount] == 1) {
                // If touch is a single tap, store its location
                // so it can be averaged with the second touch location
                tapLocation = [touch locationInView:self];
            } else {
                twoFingerTapIsPossible = NO;
            }
        }
 
        // Case 3: this is the end of the second of the two touches
        else if ([touches count] == 1 && allTouchesEnded) {
            UITouch *touch = [touches anyObject];
            if ([touch tapCount] == 1) {
                // if the last touch up is a single tap, this was a 2-finger tap
                tapLocation = midpointBetweenPoints(tapLocation,
                                                    [touch locationInView:self]);
                [self handleTwoFingerTap];
            }
        }
    }
 
    // if all touches are up, reset touch monitoring state
    if (allTouchesEnded) {
        twoFingerTapIsPossible = YES;
        multipleTouches = NO;
    }
}

The touchesCancelled:withEvent: Implementation

The view receives a touchesCancelled:withEvent: message if the scroll view detects that the tap handling is no longer relevant because the finger has moved, which causes a scroll to begin. This method simply resets the state variables.

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    twoFingerTapIsPossible = YES;
    multipleTouches = NO;
}

The ScrollView Suite Example

The ScrollViewSuite sample code project has excellent examples of implementing zooming using tap gestures. The TapToZoom example in the suite implements a subclass of UIImageView that supports zooming using the tapping behavior as displayed in the Maps application. The implementation is generic enough, through it’s use of a delegate (typically the controller that manages the scroll view) to implement the actual handling of a tap, double-tap, or double touch, that you should be able to easily adapt the code, and design, to your own views.

The TapDetectingImageView class is the subclass of UIImageView that implements the touch handling, using the RootViewController class as the delegate that handles the actual tap and touch responses, as well as the controller that initially configures the UIScrollView.