Implementing Tap to Zoom in UIScrollView on an iPhone 20

Posted by jonathan on December 11, 2008

I just spent the past two days wrestling with trying to get a working implementation of tap to zoom working correctly in a UIScrollView.

I’m writing an iPhone application that generates a report view, and I really need to allow users to tap to zoom in, tap to zoom out, and still be able to use pinch zooming and panning.

A long search of the Apple demo code, and internet articles did not turn anything definitive up, besides the fact that almost everyone else seems to be having similar problems, including but not limited to: fuzzy renderings when expanding, confused and incorrectly zooming UIScrollViews after manual tap zooming, and much else besides.

I ran into all these problems, and eventually I concluded that the layers underneath a UIView are used to cache zoom renderings, and also carry other state, that it just is not possible to get at through the published api. There *is* a private api, but it is just that, private, and I’d like to get this app on the App Store, so I can’t use it.

Ok, so the approach I created is simply to recreate the scroll view and content view whenever I need to. It turns out that this works well enough for me, both in the simulator, and on the device.

The following is my TapZoomDemo I created to show this solution:

TapZoomAppDelegate.h

//
//  TapZoomDemoAppDelegate.h
//  TapZoomDemo
//
//  Created by Jonathan Watmough on 12/10/08.
//  Copyright __MyCompanyName__ 2008. All rights reserved.
//

#import 

@interface TapZoomDemoAppDelegate : NSObject  
{
    UIWindow *window;
}

// properties
@property (nonatomic, retain) IBOutlet UIWindow *window;

// methods
- (void)createScrollView:(CGRect)scrollFrame contentOffset:(CGPoint)contentOffset contentFrame:(CGRect)contentFrame
           scalingFactor:(float)scalingFactor;


@end

TapZoomAppDelegate.m

//
//  TapZoomDemoAppDelegate.m
//  TapZoomDemo
//
//  Created by Jonathan Watmough on 12/10/08.
//  Copyright __MyCompanyName__ 2008. All rights reserved.
//

#import "TapZoomDemoAppDelegate.h"
#import "MyContentView.h"

@implementation TapZoomDemoAppDelegate

@synthesize window;

// content min and max widths (used to calculate min/max scale factors)
// hardcoded to same as and 2x width of scroll view
static const float kMinWidth = 320;
static const float kMaxWidth = 640;

- (void)dealloc 
{
    [window release];
    [super dealloc];
}

- (void)applicationDidFinishLaunching:(UIApplication *)application {

    // Override point for customization after application launch
    
    // put up a backing checked view
    UIView * checks = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
    checks.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"4x4check.png"]];
    [window addSubview:checks];
    
    // use app frame for scroll view
    CGRect scrollFrame = [[UIScreen mainScreen] applicationFrame];
    
    // display our view initially at max size (2x), offset to be centered
    CGRect contentFrame = CGRectMake(0, 0, scrollFrame.size.width*2, scrollFrame.size.height*2);
    CGPoint contentOffset = CGPointMake((contentFrame.size.width-scrollFrame.size.width)/2,
                                        (contentFrame.size.height-scrollFrame.size.height)/2);

    // create scroll view and content view
    [self createScrollView:scrollFrame contentOffset:contentOffset contentFrame:contentFrame scalingFactor:0.0];

    // register for double-tap on content view
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(doubleTapZoom:) 
                                                 name:@"DoubleTapZoom" object:nil];
    // bring up app window
    [window makeKeyAndVisible];
}

// helper method for creating a scroll view and content view
- (void)createScrollView:(CGRect)scrollFrame contentOffset:(CGPoint)contentOffset contentFrame:(CGRect)contentFrame
           scalingFactor:(float)scalingFactor
{
    // create content frame at required size, with optional scaling
    MyContentView * content = [[MyContentView alloc] initWithFrame:contentFrame];
    if( scalingFactor!=0 )
        [content setTransform:CGAffineTransformMakeScale(scalingFactor, scalingFactor)];
    
    // create a new scroll view and add to window
    UIScrollView * scrollView = [[UIScrollView alloc] initWithFrame:scrollFrame];
    [window addSubview:scrollView];
    
    // reset min and max scaling
    [scrollView setMinimumZoomScale:kMinWidth/contentFrame.size.width];
    [scrollView setMaximumZoomScale:kMaxWidth/contentFrame.size.width];
    
    // constrain the content offset
    float offsetX = contentOffset.x;
    if( offsetX<0 )
        offsetX = 0;
    if( offsetX>contentFrame.size.width-scrollFrame.size.width )
        offsetX = contentFrame.size.width-scrollFrame.size.width;
    float offsetY = contentOffset.y;
    if( offsetY<0 )
        offsetY = 0;
    if( offsetY>contentFrame.size.height-scrollFrame.size.height )
        offsetY = contentFrame.size.height-scrollFrame.size.height;
    
    // setup the scroll view, size is passed, (possible bad) offset is passed
    // we'll go ahead and use the passed offset, but animate back to constrained
    [scrollView addSubview:content];
    [scrollView setContentSize:contentFrame.size];
    [scrollView setContentOffset:contentOffset];
    [scrollView setDelegate:self];
    
    // be nice and animate back to constrained offset, with optional scaling back to 1.0 (tap zoom)
    [UIView beginAnimations:@"" context:nil];
    [scrollView setContentOffset:CGPointMake(offsetX,offsetY)];
    if( scalingFactor!=0.0 )
        [content setTransform:CGAffineTransformIdentity];
    [UIView commitAnimations];
    
    // rather than cluttering up autorelease pool
    [content release];
    [scrollView release];
}

// scroll delegate methods

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    // return the first subview of the scroll view
    return [scrollView.subviews objectAtIndex:0];
}

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
{
    // get the view
    UIView * content = [scrollView.subviews objectAtIndex:0];
    
    // get size and offset of (dynamic scaled) content view
    CGPoint contentOffset = [scrollView contentOffset];
    CGSize contentSize = [scrollView contentSize];
    
    // dump the old views
    [content removeFromSuperview];
    [scrollView removeFromSuperview];
    
    // scale the size of the frame to scaled size
    CGRect contentFrame = CGRectMake(0, 0, contentSize.width, contentSize.height);
    
    // create a new scroll view
    CGRect scrollFrame = [[UIScreen mainScreen] applicationFrame];
    
    // create our new content view and containing scroll view
    [self createScrollView:scrollFrame contentOffset:contentOffset contentFrame:contentFrame scalingFactor:0.0];
}

// Double Tap Zoom
- (void)doubleTapZoom:(NSNotification *)notification
{
    // scroll view passed as object, get content subview
    UIScrollView * scrollView = [notification object];
    UIView * content = [[scrollView subviews] objectAtIndex:0];

    // get dictionary holding event, and event
    NSDictionary * dict = [notification userInfo];
    UIEvent * event = [dict objectForKey:@"event"];
    
    // get touch from set of touches for view
    UITouch * touch = [[event touchesForView:content] anyObject];
    
    // need scroll frame, content frame and content offset
    CGRect scrollFrame = scrollView.frame;
    CGRect contentFrame;
    CGPoint contentOffset;

    // zoom in if not at max size, other zoom out to fit to page
    float scalingFactor = 0.0;
    float currentWidth = [content frame].size.width;
    if( currentWidth < kMaxWidth )
    {
        contentFrame = CGRectMake(0, 0, scrollFrame.size.width*2, scrollFrame.size.height*2);
        scalingFactor = kMaxWidth / currentWidth;
        CGPoint touchPoint = [touch locationInView:content];
        contentOffset = CGPointMake(touchPoint.x*scalingFactor/2, touchPoint.y*scalingFactor/2);
    }
    else
    {
        scalingFactor = kMinWidth / currentWidth;
        contentFrame = CGRectMake(0, 0, scrollFrame.size.width, scrollFrame.size.height);
        contentOffset = CGPointMake(0,0);
    }
    
    // dump the old views
    [content removeFromSuperview];
    [scrollView removeFromSuperview];
    
    // create scroll view and content view
    [self createScrollView:scrollFrame contentOffset:contentOffset contentFrame:contentFrame scalingFactor:1.0/scalingFactor];
}

@end

MyContentView.m

//
//  MyContentView.m
//  TapZoomDemo
//
//  Created by Jonathan Watmough on 12/10/08.
//  Copyright 2008 __MyCompanyName__. All rights reserved.
//

#import "MyContentView.h"


@implementation MyContentView


- (id)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        // Initialization code
        [self setBackgroundColor:[UIColor whiteColor]];
    }
    return self;
}


- (void)drawRect:(CGRect)rect
{
    // Drawing code - This will be called with different bounds at the end of each
    // tap/pinch zoom operation.
    // For illustration, I'm scaling so that a '1' glyph fills the area.

    // context
    CGContextRef context = UIGraphicsGetCurrentContext();

    // Fill view with a 20-point '1'
    UIFont * f = [UIFont systemFontOfSize:20];
    [[UIColor darkGrayColor] set];
    
    // get bounds
    CGRect b = [self bounds];
    
    // draw a string
    NSString * text = @"1";
    CGSize sz = [text sizeWithFont:f];

    // scale to bounds / text size
    CGContextScaleCTM(context, b.size.width/sz.width, b.size.height/sz.height);
    
    // draw
    [text drawAtPoint:CGPointMake(0,0) withFont:f];
}

// look for double taps
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    // get any touch
    UITouch * t = [touches anyObject];
    if( [t tapCount]>1 )
    {
        NSDictionary *dict = [NSDictionary dictionaryWithObject:event forKey:@"event"];
        [[NSNotificationCenter defaultCenter] postNotificationName:@"DoubleTapZoom" 
                                                            object:[self superview]
                                                          userInfo:dict];
    }
}

- (void)dealloc {
    [super dealloc];
}


@end
Trackbacks

Use this link to trackback from your own site.

Comments

Leave a response

  1. Michal Sat, 13 Dec 2008 12:10:13 UTC

    I’m having a similar problem. It’s strange because lot of apps use tap 2 zoom and this this first working example that I found. Thanks.

  2. jonathan Sat, 13 Dec 2008 16:18:28 UTC

    Thanks, yeah, I was surprised that I couldn’t find a working example either.

    The offset calculation on the tap zoom is not quite right, but I’ll try and post a fix at some point.

  3. poojarusia Wed, 24 Dec 2008 10:32:23 UTC

    Hello
    i hav seen so many code of pinch in &pinch out code in net ..but i when i was implement they was not working on simulator ..it ‘s first program which is working well..
    thanks…….
    i hav one problem ..i want to zoom in /out my text string & its also waking by this code
    but i want to find font size of of character of text string ..when i zoom it or zoom out ..
    if u know hw do it …plz reply me as soon as..on mail id or own site..
    thank for consideration
    pooja rsuia

  4. Lastiko Thu, 01 Jan 2009 22:44:42 UTC

    hi yep , i didn’t try your code for the moment ( tomorrow i hope ) but i’m in the same idea than poojarusia :D

    thanks for this code

    john

  5. demetriusb Tue, 06 Jan 2009 07:18:03 UTC

    I tried your code and am getting errors all over the place. Is there a way to download it to see exactly how it was implemented.

    Demetrius

  6. jerry Sat, 17 Jan 2009 08:33:01 UTC

    Wooou, it’s a really owesome demo code, thank you!!!!!!!!!!!!!!!!

    I have been feazed for this problem sevral days.

  7. john Mon, 19 Jan 2009 15:12:45 UTC

    Where is MyContentView.h ?

  8. jonathan Wed, 21 Jan 2009 19:42:21 UTC

    @john

    MyContentView.h is just a plain .h file. No additions.

  9. demetriusb Sun, 01 Feb 2009 20:30:30 UTC

    Got your example to work. One question, why put all your code in the app delegate? It’s a great example though.

  10. demetriusb Mon, 23 Feb 2009 23:08:01 UTC

    What would be the best way to do this with multiple images?

  11. maddy Mon, 02 Mar 2009 07:43:52 UTC

    How to do the same with multiple images in a scroll view. ???

    The tutorial was good.

  12. Rindra Thu, 19 Mar 2009 14:54:29 UTC

    Me too, I wonder if you could help me in creating slideshow with multiple images like Photo.app(MobileSlideShow) in iPhone?Thank you.

  13. Rindra Fri, 20 Mar 2009 13:25:15 UTC

    How to implement it with multiple subviews?

  14. Shawn Stanley Sun, 26 Apr 2009 14:13:09 UTC

    Ouch, that seems like a lot of work. Why not use your scroll view’s setZoomScale:animated:?

    Rindra, just make the multiple subviews subviews of the scroll view’s content view.

  15. mehran Tue, 05 May 2009 08:11:04 UTC

    setZoomScale:animated is only available on 3.0 and above. If you want your app to work on lower versions of the OS, you need to find a different solution. (Most normal users don’t update their iPhone OS with any reasonable frequency)

  16. amalacrema Wed, 16 Feb 2011 09:51:22 UTC

    Hi,

    amazing sample!!

    thats great for a UIScrollView but in case we wanna do the same for an UIwebview? Shall we handle the scroll subview inside all UIWebviews?

  17. gonzalez Tue, 24 May 2011 05:29:39 UTC

    Where is MyContentView.m (or how can I create?)

  18. gonzalez Tue, 24 May 2011 05:49:41 UTC

    CGRect b = [self bounds];

    in this section appears me two errors

    CGRect b = [self bounds];
    Initializing ‘CGRect’ (aka struct GGRect) with an expression of incompatible type id 2

    and…12 warnings…

    Could u help me?

  19. boosa ramesh Tue, 14 May 2013 06:20:48 UTC

    CGRect b = [self bounds]; getting error where u initialize bounds

  20. boosa ramesh Tue, 14 May 2013 06:21:07 UTC

    can u respond me

Comments