Basic Zooming Using the Pinch Gestures

UIScrollView makes supporting the pinch gestures for zooming easy. Your application specifies the zoom factors (that is how much bigger or smaller you can make the content) and you implement a single delegate method. Taking those few steps allows your scroll view to support the pinch gestures for zooming.

Supporting the Pinch Zoom Gestures

The pinch-in and pinch-out zoom gestures are standard gestures that iOS application users expect to use when zooming in and out. Figure 3-1 shows examples of the pinch gestures.

Figure 3-1  The standard pinch-in and pinch-out gestures

To support zooming, you must set a delegate for your scroll view. The delegate object must conform to the UIScrollViewDelegate protocol. In many cases, the delegate will be the scroll view’s controller class. That delegate class must implement the viewForZoomingInScrollView: method and return the view to zoom. The implementation of the delegate method shown below returns the value of the controller’s imageView property, which is an instance of UIImageView. This specifies that the imageView property will be zoomed in response to zoom gestures, as well as any programmatic zooming.

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    return self.imageView;
}

To specify the amount the user can zoom, you set the values of the minimumZoomScale and maximumZoomScale properties, both of which are initially set to 1.0. The value of these properties can be set in Interface Builder’s UIScrollView inspector panel, or programmatically. Listing 3-1 shows the code required in a subclass of UIViewController to support zooming. It assumes that the instance of the controller subclass is the delegate and implements the viewForZoomingInScrollView: delegate method shown above.

Listing 3-1  The UIViewController subclass implementation of the minimum required zoom methods

- (void)viewDidLoad {
    [super viewDidLoad];
    self.scrollView.minimumZoomScale=0.5;
    self.scrollView.maximumZoomScale=6.0;
    self.scrollView.contentSize=CGSizeMake(1280, 960);
    self.scrollView.delegate=self;
}

Specifying the zoom factor and the delegate object that implements the viewForZoomingInScrollView: method are the minimum requirements to support zooming using the pinch gestures.

Zooming Programmatically

A scroll view may need to zoom in response to touch events, such as double taps or other tap gestures, or in response to another user action other than a pinch gesture. To allow this, UIScrollView provides implementations of two methods: setZoomScale:animated: and zoomToRect:animated:.

The setZoomScale:animated: sets the current zoom scale to the specified value. The value must be within the range specified by the minimumZoomScale and maximumZoomScale range. If the animated parameter is YES, the zoom performs a constant animation until it reaches its completion; otherwise the change in scale is immediate. It is also possible to set the zoomScale property directly. This is the equivalent of calling setZoomScale:animated: passing NO as the animated parameter. When zooming using this method, or by changing the property directly, the view will be zoomed such that the center of the view remains stationary.

The zoomToRect:animated: method zooms the content in order to fill the specified rectangle. As with setZoomScale:animated: this method has an animated parameter that determines whether the change in location and zoom results in animation taking place.

Your application will often want to set the zoom scale and location in response to a tap at a specific location. Because setZoomScale:animated: zooms around the center of the visible content, you will need a function that will take a specific location and zoom factor and convert that to a rectangle that is appropriate to zoomToRect:animated:. A utility method that takes a scroll view, a zoom scale, and a point to center the zoom rect on is shown in Listing 3-2.

Listing 3-2  A utility method that converts a specified scale and center point to a rectangle for zooming

- (CGRect)zoomRectForScrollView:(UIScrollView *)scrollView withScale:(float)scale withCenter:(CGPoint)center {
 
    CGRect zoomRect;
 
    // The zoom rect is in the content view's coordinates.
    // At a zoom scale of 1.0, it would be the size of the
    // imageScrollView's bounds.
    // As the zoom scale decreases, so more content is visible,
    // the size of the rect grows.
    zoomRect.size.height = scrollView.frame.size.height / scale;
    zoomRect.size.width  = scrollView.frame.size.width  / scale;
 
    // choose an origin so as to get the right center.
    zoomRect.origin.x = center.x - (zoomRect.size.width  / 2.0);
    zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);
 
    return zoomRect;
}

This utility method is useful when responding to a double tap in a custom subclass that supports that gesture. To use this method simply pass the relevant UIScrollView instance, the new scale (often derived from the existing zoomScale by adding or multiplying a zoom amount), and the point around which to center the zoom. When responding to a double tap gesture the center point is typically the location of the tap. The rectangle that is returned by the method is suitable for passing to the zoomToRect:animated: method.

Informing the Delegate that the Zoom is Finished

When the user has completed the zoom pinch gestures or the programmatic zooming of the scroll view is completed, the UIScrollView delegate is informed by receiving a scrollViewDidEndZooming:withView:atScale: message.

This method is passed the scroll view instance, the scroll view subview that has been scrolled, and the scale factor at which the zoom completed as parameters. Upon receiving this delegate message your application can then take the appropriate action.

Ensuring that Zoomed Content is Sharp when Zoomed

When the content of a scroll view is zoomed, the content of the zoom view is simply scaled in response to the change in the scroll factor. This creates in larger or smaller, content, but doesn’t cause the content to redraw. As a result the displayed content is not displayed sharply. When the zoomed content is an image, and your application doesn’t display new, more detailed content, such as the Maps application, this may not be an issue.

If your application does need to display more detailed bitmap images in response to zooming you may want to examine the Tiling example in the ScrollViewSuite sample code. It uses a technique of pre-rendering the zoomed content in small chunks, and then displays those in separate views in the scroll view.

However, if your zoomed content is drawn in real time and needs to be displayed sharply when zoomed, your application class that draws the zoomed view needs to use Core Animation. The class needs to change the Core Animation class used as the UIView class’s layer to CATiledLayer and draw using the Core Animation drawLayer:inContext: method.

Listing 3-3 shows a complete implementation of a subclass that will draw a cross and allow you to zoom. The image remains sharp throughout the zoom action. The Zoomable view is a UIView subclass that has been added as a subview of a scroll view with a content size of (460,320) and is returned by the scroll view delegate method viewForZoomingInScrollView:. No action on the developer’s part is required to cause the Zoomable view to be redrawn when zooming occurs.

Listing 3-3  Implementation of a UIView Subclass That Draw’s Its Content Sharply During Zoom

#import "ZoomableView.h"
#import <QuartzCore/QuartzCore.h>
 
@implementation ZoomableView
 
 
// Set the UIView layer to CATiledLayer
+(Class)layerClass
{
    return [CATiledLayer class];
}
 
 
// Initialize the layer by setting
// the levelsOfDetailBias of bias and levelsOfDetail
// of the tiled layer
-(id)initWithFrame:(CGRect)r
{
    self = [super initWithFrame:r];
    if(self) {
        CATiledLayer *tempTiledLayer = (CATiledLayer*)self.layer;
        tempTiledLayer.levelsOfDetail = 5;
        tempTiledLayer.levelsOfDetailBias = 2;
        self.opaque=YES;
    }
    return self;
}
 
// Implement -drawRect: so that the UIView class works correctly
// Real drawing work is done in -drawLayer:inContext
-(void)drawRect:(CGRect)r
{
}
 
-(void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
    // The context is appropriately scaled and translated such that you can draw to this context
    // as if you were drawing to the entire layer and the correct content will be rendered.
    // We assume the current CTM will be a non-rotated uniformly scaled
 
   // affine transform, which implies that
    // a == d and b == c == 0
    // CGFloat scale = CGContextGetCTM(context).a;
    // While not used here, it may be useful in other situations.
 
    // The clip bounding box indicates the area of the context that
    // is being requested for rendering. While not used here
    // your app may require it to do scaling in other
    // situations.
    // CGRect rect = CGContextGetClipBoundingBox(context);
 
    // Set and draw the background color of the entire layer
    // The other option is to set the layer as opaque=NO;
    // eliminate the following two lines of code
    // and set the scroll view background color
    CGContextSetRGBFillColor(context, 1.0,1.0,1.0,1.0);
    CGContextFillRect(context,self.bounds);
 
    // draw a simple plus sign
    CGContextSetRGBStrokeColor(context, 0.0, 0.0, 1.0, 1.0);
    CGContextBeginPath(context);
    CGContextMoveToPoint(context,35,255);
    CGContextAddLineToPoint(context,35,205);
    CGContextAddLineToPoint(context,135,205);
    CGContextAddLineToPoint(context,135,105);
    CGContextAddLineToPoint(context,185,105);
    CGContextAddLineToPoint(context,185,205);
    CGContextAddLineToPoint(context,285,205);
    CGContextAddLineToPoint(context,285,255);
    CGContextAddLineToPoint(context,185,255);
    CGContextAddLineToPoint(context,185,355);
    CGContextAddLineToPoint(context,135,355);
    CGContextAddLineToPoint(context,135,255);
    CGContextAddLineToPoint(context,35,255);
    CGContextClosePath(context);
 
    // Stroke the simple shape
    CGContextStrokePath(context);
 
 
}