Commits

Bill Garrison committed 1f736b5

Renamed custom view class. Exposing tap gesture recognizers to callersallows an enclosing view controller to add its own actions that fire when the gesture triggers

Comments (0)

Files changed (9)

Classes/RootViewController.m

 @implementation RootViewController
 
 #pragma mark -
+#pragma mark Properties
+
+@synthesize landscapeView, portraitView;
+
+#pragma mark -
 #pragma mark NSObject Overrides
 
+- (void) viewDidUnload
+{
+    [super viewDidUnload];
+
+    landscapeView = nil;
+    portraitView = nil;
+}
+
 - (void) dealloc 
 {	
 	// We're no longer interested in any notifications.
 #pragma mark -
 #pragma mark UIViewController Overrides
 
-- (void)viewDidLoad 
+- (void) viewDidLoad 
 {
     [super viewDidLoad];
 	self.title = NSLocalizedString(@"Tap Zoom Image Viewer Demo", nil);
 	[self.navigationController pushViewController:imageViewer animated:YES];
 }
 
-#pragma mark -
-#pragma mark TapDetectingImageViewDelegate Methods
-
-#if 0
-- (void)tapDetectingImageView:(TapDetectingImageView *)view gotSingleTapAtPoint:(CGPoint)tapPoint;
-{
-	LOG_ENTRY;
-	
-	// We use TapDetectingImageView to implement image button behavior. A regular button with an image didn't display the images correctly...
-	// tag 100 is the small image; tag 200 is the large image.
-	if ( [view tag] == 100 ) {
-		[self showSmallImageInViewer:nil];
-	}
-	else if ( [view tag] == 200 ) {
-		[self showLargeImageInViewer:nil];
-	}
-}
-#endif
-
-#pragma mark -
-#pragma mark Properties
-
-@synthesize landscapeView, portraitView;
-
 @end
 

Classes/SOPanZoomImageView.h

-//
-//
-//  Created on 01/06/09.
-//  Copyright Standard Orbit Software, LLC 2009. All rights reserved.
-//	LICENSE <http://creativecommons.org/licenses/BSD/>
-
-
-/**
-Subclass of UIScrollView that handles zooming and centering an embedded image view.
-
-Zooming Methods:
--zoomToCenterWithScale:animated: zooms the image view to the given scale factor, and keeps the image centered when it is smaller than the scroll view bounds.
-
--configureZoomingScale recalculates minimum, maximum, and the necessary zoom-to-fit scale factors as determined by the size of the image compared to the scroll view and the device's  orientation.
-
- -isZoomedToFit reports when the scroll view is zoomed such that the image just fits on the screen in its device orientation.
-
-*/
-
-@interface SOPanZoomImageView : UIScrollView
-{
-@private    
-	
-	UIImageView *_imageView;
-	UIImage *_image;
-	
-	// Zooming
-	CGFloat _zoomToFitScaleFactor;
-}
-
-#pragma mark -
-#pragma mark Zooming and Centering
-
-/**
-\brief Update the scroll view's content inset to center the zoomable view.
-
-When the image view is smaller than the receiver's content area, this method calculates and sets the content edge insets required to keep the view centered.
-*/
-- (void) updateContentInsetForCentering;
-
-
-/**
- Zoom and center the image view.
- \param animate YES to animate the zoom and centering.
- */
-- (void) zoomToCenterWithScale:(CGFloat)scale animated:(BOOL)animate;
-
-/**
- The zoom scale required to fit the image completely within the scroll view's bounds.
- The value is recalculated whenever the device changes between horizontal and vertical orientation. 
- */
-- (void) configureZoomingScale;
-
-
-/**
- \return YES if scroll view zoom is at the zoom-to-fit value for the current orientation.
- */
-- (BOOL) isZoomedToFit;
-
-#pragma mark -
-#pragma mark Properties
-
-/** The Image we're displaying. */
-@property (nonatomic, retain) UIImage *image;
-
-/** The embedded image view.
-*/
-@property (nonatomic, readonly) UIImageView *imageView;
-
-
-/** 
-\brief The zoom scale required to fit the image completely within the scroll view's bounds.
-\desc This zoom scale can be used to zoom the scroll view so that the image just fits within the display. The value is recalculated whenever the device changes between horizontal and vertical orientation. 
- */
-@property (nonatomic, readonly) CGFloat zoomToFitScale;
-
-/**
-\return YES if the scroll view zooming scale is such that the image is zoomed to fit the current device orientation.
-*/
-@property (nonatomic, readonly) BOOL isZoomedToFit;
-
-@end
-
-#pragma mark -
-#pragma mark Constants
-
-extern NSString * const ScrollableImageViewZoomAndCenterAnimation;
-

Classes/SOPanZoomImageView.m

-
-//
-//  Created on 01/06/09.
-//  Copyright Standard Orbit Software, LLC 2009. All rights reserved.
-//	LICENSE <http://creativecommons.org/licenses/BSD/>
-
-#import "SOPanZoomImageView.h"
-
-#define ZOOM_MAX_FACTOR 10
-
-@implementation SOPanZoomImageView
-
-#pragma mark -
-#pragma mark Initialization
-
-- (void) commonInit;
-{
-	LOG_ENTRY;
-	
-	// Init an image view and install in the scroller
-	{
-		_imageView = [[UIImageView alloc] initWithFrame:CGRectZero];
-		[_imageView setAutoresizingMask:UIViewAutoresizingNone];
-		[_imageView setUserInteractionEnabled:NO];
-		[_imageView setMultipleTouchEnabled:NO];
-	}
-	
-	// Init a scroll view and install in our view.
-	{		
-		// The scroll view resizes every which way but loose
-		[self setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin];
-		[self setContentMode: UIViewContentModeRedraw];
-        
-		// Set contentSize so that scrollers accurately reflect the visible portion of the image.
-		self.contentSize = _imageView.frame.size;
-        
-		[self addSubview:_imageView];
-	}
-	
-	// We're very interactive
-	[self setUserInteractionEnabled:YES];
-	[self setMultipleTouchEnabled:YES];
-	
-	_zoomToFitScaleFactor = 1.0; // reasonable default
-	
-	// Listen for device orientation changes
-	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil];
-}
-
-- (void) awakeFromNib
-{	
-	LOG_ENTRY;
-	[super awakeFromNib];
-	[self commonInit];
-	
-    //	[self.imageView logResponderChain];
-}
-
-
-/** Our designated initializer */
-
-- (id) initWithImage:(UIImage *)image
-{
-	LOG_ENTRY;
-	CGRect frame = (CGRect){.size = image.size};
-	self = [super initWithFrame:frame];
-	if (!self) return nil;
-    
-    _image = image;
-    [self commonInit];
-	
-	return self;
-}
-
-// UIView designated initializer override
-- (id) initWithFrame:(CGRect)frame
-{
-	LOG_ENTRY;
-    return [self initWithImage:nil];
-}
-
-- (id) init;
-{
-	LOG_ENTRY;
-	return [self initWithFrame:CGRectZero];
-}
-
-- (void) dealloc;
-{
-	LOG_EXIT;
-	
-    _image = nil;
-    _imageView = nil;
-}
-
-#pragma mark -
-#pragma mark UIView Overrides
-
-- (void) willMoveToSuperview:(UIView *)newSuperview
-{
-	LOG_ENTRY;
-	
-	if ( newSuperview == nil ) 
-	{
-		// We're being pull out of the view hierarchy.  Cleanup this notification registration.
-		[[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
-	}
-	
-	LOG_EXIT;
-}
-
-- (BOOL) isOpaque
-{
-	return YES;
-}
-
-// Must override -canBecomeFirstResponder so that we can participate in the responder chain
-- (BOOL) canBecomeFirstResponder 
-{
-    return YES;
-}
-
-#pragma mark -
-#pragma mark Observers and Updating
-
-// Here's where we change our root view to accomodate the new device orientation.
-
-- (void) orientationChanged:(NSNotification *)notification
-{
-	LOG_ENTRY;
-	
-	BOOL isLandscape = UIDeviceOrientationIsLandscape( [[UIDevice currentDevice] orientation] );
-	BOOL isPortrait = UIDeviceOrientationIsPortrait( [[UIDevice currentDevice] orientation] );
-#if DEBUG
-	NSLog(@"isLandscape: %@", StringFromBool(isLandscape));
-	NSLog(@"isPortrait: %@", StringFromBool(isPortrait));
-#endif
-    
-	if ( ! (isLandscape || isPortrait) ) {
-		// Only respond to this orientation change if the device has switched from portrait to landscape orientation, or vice-versa.  Ignore other orientations.
-		return;
-	}
-    
-	// Capture this condition before we recalculate ZTF.
-	BOOL isZTF = [self isZoomedToFit];
-	
-	// Resize our frame to match the orientation
-	self.frame = self.superview.bounds;
-	
-	// Reconfigure the min, max, and zoom-to-fit zooming scale values to accomodate the change in orientation.
-	[self configureZoomingScale];
-	
-	/* 
-     Special case when the zoom scale value is equal to zoom-to-fit: preserve the ZTF condition after changing orientation, as appropriate for the new orientation.  
-     That is, if the user has the image zoomed to fit in one orientation, it will continue to be zoomed to fit to the new orientation. 
-     The actual zoom scale value will change, but the image will be zoomed to fit as appropriate for the new landscape or portrait orientation.
-     */
-	
-	if ( isZTF ) 
-    {
-		// Always keep zoomed-to-fit
-		[self zoomToCenterWithScale: self.zoomToFitScale animated:YES];
-	} 
-    else
-    {
-		// Keep the user's current zoom value.
-		[self zoomToCenterWithScale:self.zoomScale animated:YES];
-	}
-	
-}
-
-- (void) updateLayoutForNewImage
-{
-	// Install the new image in the image view.
-	self.imageView.image = self.image;
-	[self.imageView sizeToFit];
-	
-	// Center the image
-	[self configureZoomingScale];
-	[self zoomToCenterWithScale:self.zoomToFitScale animated:YES];
-	
-	// Update the scroll view's content size so that scroll bars are properly sized.
-	self.contentSize = self.imageView.frame.size;	
-}
-
-#pragma mark -
-#pragma mark Zooming and Centering
-
-- (void) updateContentInsetForCentering
-{	
-	//	LOG_ENTRY;
-	
-	// Calculate edge insets to keep the image view centered
-	
-	UIEdgeInsets insetForCentering = { 0, 0, 0, 0};
-	{		
-		
-		// Does frame need to be centered horizontally?  If it is less wide than the scrollview width, yes.
-		CGFloat deltaWidth = CGRectGetWidth(self.bounds) - CGRectGetWidth(self.imageView.frame);
-		CGFloat deltaHeight = CGRectGetHeight(self.bounds) - CGRectGetHeight(self.imageView.frame);
-		
-		if (deltaWidth > 0)
-		{
-			// Split the delta width between the left and right sides
-			insetForCentering.left = deltaWidth / 2.0;
-			insetForCentering.right = -insetForCentering.left;
-		}
-		
-		// Does frame need to be centered vertically? If it is less tall than the scrollview height, yes.
-		if (deltaHeight > 0)
-		{
-			// Split the delta height between the top and bottom sides
-			insetForCentering.top = deltaHeight / 2.0;
-			insetForCentering.bottom = -insetForCentering.top;
-		}
-	}
-	
-	// Inset the image view within the scroll view to center it
-	if ( ! UIEdgeInsetsEqualToEdgeInsets (insetForCentering, self.contentInset)) 
-    {
-		self.contentInset = insetForCentering;
-	}
-}
-
-- (void) zoomToCenterWithScale:(CGFloat)scale animated:(BOOL)animate;
-{
-	// If animating, start an animation block.  
-	
-	// Future Developer: Note that the zoom and the centering are both animated when enclosed in the animation block.
-	// If you remove the use of the block and just ask for the zoom to animate, the resulting UI is jerky. 
-	
-	if (animate) [UIView beginAnimations:ScrollableImageViewZoomAndCenterAnimation context:NULL];
-	
-	// Re-zoom the image view and re-center in the scroll view
-	
-	[self setZoomScale:scale animated:NO];
-	[self updateContentInsetForCentering];
-	
-	// If animating, kick off the animations.
-	if (animate) [UIView commitAnimations];
-}
-
-- (void) configureZoomingScale;
-{
-	LOG_ENTRY;
-	
-	// Configure the min, max, and zoom-to-fit zooming scale values.
-	
-	//NSLog(@"Before calculation: _zoomToFitScale : %f", _zoomToFitScale);
-	
-	BOOL isLandscape = UIDeviceOrientationIsLandscape( [[UIDevice currentDevice] orientation] );
-	if (isLandscape) 
-    {
-		// Display height is the constraining dimension.
-		_zoomToFitScaleFactor = CGRectGetHeight(self.frame) / self.imageView.image.size.height;
-	} 
-    else 
-    {
-		// Display width is the constraining dimension.
-		_zoomToFitScaleFactor = CGRectGetWidth(self.frame) / self.imageView.image.size.width;
-	}
-	
-	// Set the zoom scale, determining the scale factor that makes the image zoom-to-fit the display (ZTF)
-    // For small images (i.e. images that will naturally fit within the bounds of the scroll view), use 1.0
-
-    self.minimumZoomScale = 1.0;
-
-	if (_zoomToFitScaleFactor < 1.0)
-    {
-		// For large images, the zoom-to-fit scale value will be the minimum
-		self.minimumZoomScale = _zoomToFitScaleFactor;
-	}
-
-	
-	self.maximumZoomScale = ZOOM_MAX_FACTOR;
-	
-	//NSLog(@"After calculation: _zoomToFitScale: %f", _zoomToFitScale);
-}
-
-#pragma mark -
-#pragma mark Properties
-
-@synthesize image = _image;
-@synthesize imageView = _imageView;
-@synthesize zoomToFitScale = _zoomToFitScaleFactor;
-
-- (BOOL) isZoomedToFit;
-{
-	BOOL isZTF = fabs(self.zoomScale - self.zoomToFitScale) < 0.00001;
-	return isZTF;
-}
-
-- (void) setImage:(UIImage *)newImage;
-{
-	if ( newImage != _image ) 
-    {
-		_image = newImage;
-		
-		[self updateLayoutForNewImage];
-	}
-}
-
-@end
-
-#pragma mark -
-#pragma mark Constants
-
-NSString * const ScrollableImageViewZoomAndCenterAnimation = @"ZoomAndCenterAnimation";

Classes/SOTapPanZoomImageView.h

+//
+//
+//  Created on 01/06/09.
+//  Copyright Standard Orbit Software, LLC 2009. All rights reserved.
+//	LICENSE <http://creativecommons.org/licenses/BSD/>
+
+
+/**
+Subclass of UIScrollView that handles zooming and centering an embedded image view.
+
+Zooming Methods:
+-zoomToCenterWithScale:animated: zooms the image view to the given scale factor, and keeps the image centered when it is smaller than the scroll view bounds.
+
+-configureZoomingScale recalculates minimum, maximum, and the necessary zoom-to-fit scale factors as determined by the size of the image compared to the scroll view and the device's  orientation.
+
+ -isZoomedToFit reports when the scroll view is zoomed such that the image just fits on the screen in its device orientation.
+
+ Tap Gestures:
+ 
+SOTapPanZoomImageView registers a single, double, and two-finger tap gesture recognizer. Callers can access these gesture recognizers to add their own target-action pairs.
+ 
+ -singleTap
+ -doubleTap
+ -twoFingerTap
+ 
+*/
+
+@interface SOTapPanZoomImageView : UIScrollView
+{
+@private    
+	
+	UIImageView *_imageView;
+	UIImage *_image;
+	
+	// Zooming
+	CGFloat _zoomToFitScaleFactor;
+}
+
+@property (nonatomic, strong) UITapGestureRecognizer *singleTap;
+@property (nonatomic, strong) UITapGestureRecognizer *doubleTap;
+@property (nonatomic, strong) UITapGestureRecognizer *twoFingerTap;
+
+#pragma mark -
+#pragma mark Zooming and Centering
+
+/**
+\brief Update the scroll view's content inset to center the zoomable view.
+
+When the image view is smaller than the receiver's content area, this method calculates and sets the content edge insets required to keep the view centered.
+*/
+- (void) updateContentInsetForCentering;
+
+
+/**
+ Zoom and center the image view.
+ \param animate YES to animate the zoom and centering.
+ */
+- (void) zoomToCenterWithScale:(CGFloat)scale animated:(BOOL)animate;
+
+/**
+ The zoom scale required to fit the image completely within the scroll view's bounds.
+ The value is recalculated whenever the device changes between horizontal and vertical orientation. 
+ */
+- (void) configureZoomingScale;
+
+
+/**
+ \return YES if scroll view zoom is at the zoom-to-fit value for the current orientation.
+ */
+- (BOOL) isZoomedToFit;
+
+#pragma mark -
+#pragma mark Properties
+
+/** The Image we're displaying. */
+@property (nonatomic, retain) UIImage *image;
+
+/** The embedded image view.
+*/
+@property (nonatomic, readonly) UIImageView *imageView;
+
+
+/** 
+\brief The zoom scale required to fit the image completely within the scroll view's bounds.
+\desc This zoom scale can be used to zoom the scroll view so that the image just fits within the display. The value is recalculated whenever the device changes between horizontal and vertical orientation. 
+ */
+@property (nonatomic, readonly) CGFloat zoomToFitScale;
+
+/**
+\return YES if the scroll view zooming scale is such that the image is zoomed to fit the current device orientation.
+*/
+@property (nonatomic, readonly) BOOL isZoomedToFit;
+
+@end
+
+#pragma mark -
+#pragma mark Constants
+
+extern NSString * const ScrollableImageViewZoomAndCenterAnimation;
+

Classes/SOTapPanZoomImageView.m

+
+//
+//  Created on 01/06/09.
+//  Copyright Standard Orbit Software, LLC 2009. All rights reserved.
+//	LICENSE <http://creativecommons.org/licenses/BSD/>
+
+#import "SOTapPanZoomImageView.h"
+
+#define ZOOM_MAX_FACTOR 10
+#define ZOOM_OUT_FACTOR 2
+
+@implementation SOTapPanZoomImageView
+
+@synthesize singleTap = _singleTap;
+@synthesize doubleTap = _doubleTap;
+@synthesize twoFingerTap = _twoFingerTap;
+
+#pragma mark -
+#pragma mark Initialization
+
+- (void) commonInit;
+{
+	LOG_ENTRY;
+	
+	// Init an image view and install in the scroller
+	{
+		_imageView = [[UIImageView alloc] initWithFrame:CGRectZero];
+		[_imageView setAutoresizingMask:UIViewAutoresizingNone];
+		[_imageView setUserInteractionEnabled:NO];
+		[_imageView setMultipleTouchEnabled:NO];
+        [self addSubview:_imageView];
+	}
+	
+	// Init a scroll view and install in our view.
+		
+		// The scroll view resizes every which way but loose
+		[self setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin];
+		[self setContentMode: UIViewContentModeRedraw];
+        
+		// Set contentSize so that scrollers accurately reflect the visible portion of the image.
+    [self setContentSize: _imageView.frame.size];
+
+	
+	// We're very interactive
+	[self setUserInteractionEnabled:YES];
+	[self setMultipleTouchEnabled:YES];
+	
+	_zoomToFitScaleFactor = 1.0; // reasonable default
+	
+
+    
+    /* Install tap gesture recognizers */
+    
+    _doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)];
+    [_doubleTap setNumberOfTapsRequired:2];
+    [self addGestureRecognizer:_doubleTap];
+    
+    _singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)];
+    [self addGestureRecognizer:_singleTap];
+    [_singleTap requireGestureRecognizerToFail:_doubleTap];
+    
+   _twoFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwoFingerTap:)];
+    [_twoFingerTap setNumberOfTapsRequired:2];
+    [_twoFingerTap setNumberOfTouchesRequired:2];
+    [self addGestureRecognizer:_twoFingerTap];
+}
+
+- (void) awakeFromNib
+{	
+	LOG_ENTRY;
+	[super awakeFromNib];
+	[self commonInit];
+	
+    //	[self.imageView logResponderChain];
+}
+
+
+/** Our designated initializer */
+
+- (id) initWithImage:(UIImage *)image
+{
+	LOG_ENTRY;
+	CGRect frame = (CGRect){.size = image.size};
+	self = [super initWithFrame:frame];
+	if (!self) return nil;
+    
+    _image = image;
+    [self commonInit];
+	
+	return self;
+}
+
+// UIView designated initializer override
+- (id) initWithFrame:(CGRect)frame
+{
+	LOG_ENTRY;
+    return [self initWithImage:nil];
+}
+
+- (id) init;
+{
+	LOG_ENTRY;
+	return [self initWithFrame:CGRectZero];
+}
+
+- (void) dealloc;
+{
+    _singleTap = nil;
+    _doubleTap = nil;
+    _twoFingerTap = nil;
+    
+    _image = nil;
+    _imageView = nil;
+    
+    LOG_EXIT;
+}
+
+#pragma mark -
+#pragma mark UIView Overrides
+
+- (void) willMoveToSuperview:(UIView *)newSuperview
+{
+	LOG_ENTRY;
+	
+	if ( newSuperview == nil ) 
+	{
+		// We're being pull out of the view hierarchy.  Cleanup this notification registration.
+        [self removeGestureRecognizer:_singleTap];
+        [self removeGestureRecognizer:_doubleTap];
+        [self removeGestureRecognizer:_twoFingerTap];
+        
+		[[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
+	}
+
+	LOG_EXIT;
+}
+
+- (BOOL) isOpaque
+{
+	return YES;
+}
+
+// Must override -canBecomeFirstResponder so that we can participate in the responder chain
+- (BOOL) canBecomeFirstResponder 
+{
+    return YES;
+}
+
+#pragma mark -
+#pragma mark Observers and Updating
+
+// Here's where we change our root view to accomodate the new device orientation.
+
+- (void) orientationChanged:(NSNotification *)notification
+{
+	LOG_ENTRY;
+	
+	BOOL isLandscape = UIDeviceOrientationIsLandscape( [[UIDevice currentDevice] orientation] );
+	BOOL isPortrait = UIDeviceOrientationIsPortrait( [[UIDevice currentDevice] orientation] );
+#if DEBUG
+	NSLog(@"isLandscape: %@", StringFromBool(isLandscape));
+	NSLog(@"isPortrait: %@", StringFromBool(isPortrait));
+#endif
+    
+	if ( ! (isLandscape || isPortrait) ) 
+    {
+		// Only respond to this orientation change if the device has switched from portrait to landscape orientation, or vice-versa.  Ignore face up/face down orientation changes.
+		return;
+	}
+    
+	// Capture this condition before we recalculate ZTF.
+	BOOL shouldMaintainZTFAfterOrientationChange = [self isZoomedToFit];
+	
+	// Resize our frame to match the orientation
+	self.frame = self.superview.bounds;
+	
+	// Reconfigure the min, max, and zoom-to-fit zooming scale values to accomodate the change in orientation.
+	[self configureZoomingScale];
+	
+	/* 
+     Special case when the zoom scale value is equal to zoom-to-fit: preserve the ZTF condition after changing orientation, as appropriate for the new orientation.  
+     That is, if the user has the image zoomed to fit in one orientation, it will continue to be zoomed to fit to the new orientation. 
+     The actual zoom scale value will change, but the image will be zoomed to fit as appropriate for the new landscape or portrait orientation.
+     */
+	
+	if ( shouldMaintainZTFAfterOrientationChange ) 
+    {
+		// Always keep zoomed-to-fit
+		[self zoomToCenterWithScale: self.zoomToFitScale animated:YES];
+	} 
+    else
+    {
+		// Keep the user's current zoom value.
+		[self zoomToCenterWithScale:self.zoomScale animated:YES];
+	}
+	
+}
+
+- (void) updateLayoutForNewImage
+{
+	// Install the new image in the image view.
+	self.imageView.image = self.image;
+	[self.imageView sizeToFit];
+	
+	// Center the image
+	[self configureZoomingScale];
+	[self zoomToCenterWithScale:self.zoomToFitScale animated:YES];
+	
+	// Update the scroll view's content size so that scroll bars are properly sized.
+	self.contentSize = self.imageView.frame.size;	
+}
+
+#pragma mark -
+#pragma mark Zooming and Centering
+
+- (void) updateContentInsetForCentering
+{	
+	//	LOG_ENTRY;
+	
+	// Calculate edge insets to keep the image view centered
+	
+	UIEdgeInsets insetForCentering = { 0, 0, 0, 0};
+	{		
+		
+		// Does frame need to be centered horizontally?  If it is less wide than the scrollview width, yes.
+		CGFloat deltaWidth = CGRectGetWidth(self.bounds) - CGRectGetWidth(self.imageView.frame);
+		CGFloat deltaHeight = CGRectGetHeight(self.bounds) - CGRectGetHeight(self.imageView.frame);
+		
+		if (deltaWidth > 0)
+		{
+			// Split the delta width between the left and right sides
+			insetForCentering.left = deltaWidth / 2.0;
+			insetForCentering.right = -insetForCentering.left;
+		}
+		
+		// Does frame need to be centered vertically? If it is less tall than the scrollview height, yes.
+		if (deltaHeight > 0)
+		{
+			// Split the delta height between the top and bottom sides
+			insetForCentering.top = deltaHeight / 2.0;
+			insetForCentering.bottom = -insetForCentering.top;
+		}
+	}
+	
+	// Inset the image view within the scroll view to center it
+	if ( ! UIEdgeInsetsEqualToEdgeInsets (insetForCentering, self.contentInset)) 
+    {
+		self.contentInset = insetForCentering;
+	}
+}
+
+- (void) zoomToCenterWithScale:(CGFloat)scale animated:(BOOL)animate;
+{
+	// If animating, start an animation block.  
+	
+	// Future Developer: Note that the zoom and the centering are both animated when enclosed in the animation block.
+	// If you remove the use of the block and just ask for the zoom to animate, the resulting UI is jerky. 
+	
+	if (animate) [UIView beginAnimations:ScrollableImageViewZoomAndCenterAnimation context:NULL];
+	
+	// Re-zoom the image view and re-center in the scroll view
+	
+	[self setZoomScale:scale animated:NO];
+	[self updateContentInsetForCentering];
+	
+	// If animating, kick off the animations.
+	if (animate) [UIView commitAnimations];
+}
+
+- (void) configureZoomingScale;
+{
+	LOG_ENTRY;
+	
+	// Configure the min, max, and zoom-to-fit zooming scale values.
+	
+	//NSLog(@"Before calculation: _zoomToFitScale : %f", _zoomToFitScale);
+	
+	BOOL isLandscape = UIDeviceOrientationIsLandscape( [[UIDevice currentDevice] orientation] );
+	if (isLandscape) 
+    {
+		// Display height is the constraining dimension.
+		_zoomToFitScaleFactor = CGRectGetHeight(self.frame) / self.imageView.image.size.height;
+	} 
+    else 
+    {
+		// Display width is the constraining dimension.
+		_zoomToFitScaleFactor = CGRectGetWidth(self.frame) / self.imageView.image.size.width;
+	}
+	
+	// Set the zoom scale, determining the scale factor that makes the image zoom-to-fit the display (ZTF)
+    // For small images (i.e. images that will naturally fit within the bounds of the scroll view), use 1.0
+
+    self.minimumZoomScale = 1.0;
+
+	if (_zoomToFitScaleFactor < 1.0)
+    {
+		// For large images, the zoom-to-fit scale value will be the minimum
+		self.minimumZoomScale = _zoomToFitScaleFactor;
+	}
+
+	self.maximumZoomScale = ZOOM_MAX_FACTOR;
+	
+	//NSLog(@"After calculation: _zoomToFitScale: %f", _zoomToFitScale);
+}
+
+#pragma mark -
+#pragma mark Properties
+
+@synthesize image = _image;
+@synthesize imageView = _imageView;
+@synthesize zoomToFitScale = _zoomToFitScaleFactor;
+
+- (BOOL) isZoomedToFit;
+{
+	BOOL isZTF = fabs(self.zoomScale - self.zoomToFitScale) < 0.00001;
+	return isZTF;
+}
+
+- (void) setImage:(UIImage *)newImage;
+{
+	if ( newImage != _image ) 
+    {
+		_image = newImage;
+		
+		[self updateLayoutForNewImage];
+	}
+}
+
+#pragma mark -
+#pragma mark Tap Gesture Handlers methods
+
+/*
+ For single taps, we take the image full screen, hiding the navigation controller and auxillary controls (the zoom slider).
+ For double-taps, we toggle zooming the display between the image's native size and it's zoom-to-fit-the-display scale factor.
+ For two-finger taps, we incrementally zoom out from the image until we reach the minimum scale factor.
+ */
+- (void) handleSingleTap:(id)sender
+{		
+	// Rezoom the image to accommodate the presence/absence of the nav bar.
+	[self zoomToCenterWithScale:[self zoomScale] animated:NO];
+}
+
+- (void) handleDoubleTap:(id)sender
+{	
+	BOOL isZoomedToOriginalSize = fabs([self zoomScale] - 1.0) < 0.0001;
+	
+	if ( isZoomedToOriginalSize ) 
+    {
+		// Rezoom to fit the display bounds
+		[self zoomToCenterWithScale: [self zoomToFitScale] animated:YES];
+	} 
+    else 
+    {
+		// Rezoom to the image's original size
+		[self zoomToCenterWithScale:1.0 animated:YES];
+	}
+}
+
+- (void) handleTwoFingerTap:(id)sender
+{
+	LOG_ENTRY;
+	
+	// two-finger tap zooms the image back out.
+    
+	// If already at the minimum zoom factor, we're done.
+	BOOL isMinimallyZoomed = fabs([self zoomScale] - [self minimumZoomScale]) < 0.0001;
+	if ( isMinimallyZoomed ) return;
+	
+	// Calculate the new zoom factor, ensuring it stays to the minimum
+	CGFloat newScale = [self zoomScale] / ZOOM_OUT_FACTOR;
+	
+	if ( newScale < [self minimumZoomScale] ) 
+    {
+		newScale = [self minimumZoomScale];
+	}
+
+    // Zoom the image to the tap point at the new scale value.
+    [self zoomToCenterWithScale:newScale animated:YES];
+}
+
+
+@end
+
+#pragma mark -
+#pragma mark Constants
+
+NSString * const ScrollableImageViewZoomAndCenterAnimation = @"ZoomAndCenterAnimation";

Classes/TapZoomDemoViewController.h

 //  Copyright Standard Orbit Software, LLC 2009. All rights reserved.
 //
 // LICENSE <http://creativecommons.org/licenses/BSD/>
-//
-// $Revision$
-// $Author$
-// $Date$
 
 /**
  Implements a view controller that manages zooming and tapping in a scrollable image view.  The controller keeps the image centered within the scroll view when zooming happens.
  */
 
 #import <UIKit/UIKit.h>
-#import "SOPanZoomImageView.h"
+#import "SOTapPanZoomImageView.h"
 
 @interface TapZoomDemoViewController : UIViewController
 {

Classes/TapZoomDemoViewController.m

 #define ZOOM_MAX_FACTOR 10
 
 #import "TapZoomDemoViewController.h"
-#import "SOPanZoomImageView.h"
+#import "SOTapPanZoomImageView.h"
 
 @interface TapZoomDemoViewController()
-@property (nonatomic, strong) IBOutlet SOPanZoomImageView *tapZoomImageView;
+@property (nonatomic, strong) IBOutlet SOTapPanZoomImageView *tapZoomImageView;
 @property (nonatomic, strong) IBOutlet UISlider *zoomSlider;
 - (void) updateZoomSliderUI;
 @end
 - (id) initWithImage:(UIImage *)image;
 {
 	LOG_ENTRY;
-	self = [super initWithNibName:@"ImageViewController" bundle:nil];
+	self = [super initWithNibName:nil bundle:nil];
 	if ( !self ) return nil;
 	
 	_image = image;
 {
     [self viewDidUnload];
     
-	 _image = nil;
+    _image = nil;
 	
 	LOG_EXIT;
 }
     [super viewDidLoad];
     
 	LOG_ENTRY;
-		
+    
     [[self view] setUserInteractionEnabled:YES];
     [[self view] setMultipleTouchEnabled:YES];
     
-    UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)];
-    [doubleTap setNumberOfTapsRequired:2];
-    [_tapZoomImageView addGestureRecognizer:doubleTap];
+    [[self tapZoomImageView] setIndicatorStyle:UIScrollViewIndicatorStyleWhite];
     
-    UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)];
-    [_tapZoomImageView addGestureRecognizer:singleTap];
-    [singleTap requireGestureRecognizerToFail:doubleTap];
+    /* Add our own actions to the TapPanZoom image view gesture recognizers */
+    [[[self tapZoomImageView] singleTap] addTarget:self action:@selector(handleSingleTap:)];
+    [[[self tapZoomImageView] doubleTap] addTarget:self action:@selector(syncZoomSlider:)];
+    [[[self tapZoomImageView] twoFingerTap] addTarget:self action:@selector(syncZoomSlider:)];
     
-    UITapGestureRecognizer *twoFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwoFingerTap:)];
-    [twoFingerTap setNumberOfTapsRequired:2];
-    [twoFingerTap setNumberOfTouchesRequired:2];
-    [_tapZoomImageView addGestureRecognizer:twoFingerTap];
-    
-    /* Hint to ARC for cleanup */
-    doubleTap = nil;
-    singleTap = nil;
-    twoFingerTap = nil;
-    
+
 #if DEBUG_LAYOUT
 	self.imageView.backgroundColor = [UIColor blueColor];
 	self.view.backgroundColor = [UIColor greenColor];
 	
 	// Install the image and resize the image view to fit it.
 	self.tapZoomImageView.image = self.image;
-		
+    
 	[self updateZoomSliderUI];
 }
 
 
 - (IBAction) changeZoom:(id)sender;
 {
-	[self.tapZoomImageView zoomToCenterWithScale:self.zoomSlider.value animated:NO];
+	[[self tapZoomImageView] zoomToCenterWithScale:[[self zoomSlider] value] animated:NO];
 }
 
 - (void) updateZoomSliderUI
 #pragma mark UIScrollViewDelegate methods
 
 /*
-UIScrollView implements pinch-to-zoom behavior using two delegate methods: -viewForZoomingInScrollView: and -scrollViewDidEndZooming:withView:atScale:
-*/
+ UIScrollView implements pinch-to-zoom behavior using two delegate methods: -viewForZoomingInScrollView: and -scrollViewDidEndZooming:withView:atScale:
+ */
 
 - (UIView *) viewForZoomingInScrollView:(UIScrollView *)scrollView 
 {
 	LOG_ENTRY;
 	
 	ASSERT( self.tapZoomImageView == scrollView );
-
+    
 	// Always reset the scroll view content size after zooming, so that scroll bars are rescaled appropriately.
 	scrollView.contentSize = view.frame.size;
 	
 	
 	// Update the zoom slider control after a user has performed a pinch-zoom
 	[self.zoomSlider setValue:scale animated:YES];
-
+    
 	LOG_EXIT;
 }
 
+#pragma mark -
+#pragma mark Gesture Handlers
 
-#pragma mark -
-#pragma mark Tap Gesture Handlers methods
-
-/*
-For single taps, we take the image full screen, hiding the navigation controller and auxillary controls (the zoom slider).
-For double-taps, we toggle zooming the display between the image's native size and it's zoom-to-fit-the-display scale factor.
-For two-finger taps, we incrementally zoom out from the image until we reach the minimum scale factor.
-*/
 - (void) handleSingleTap:(id)sender
-{		
-	// Toggle the nav controller hidden or visible.
-	
-	[self.navigationController setNavigationBarHidden:!self.navigationController.isNavigationBarHidden animated:YES];
-	
-	// Update the zoom slider
-	if ( self.navigationController.isNavigationBarHidden ) 
-    {
-		[self.zoomSlider setValue:_tapZoomImageView.zoomScale animated:NO];
-		self.zoomSlider.hidden = YES;
-	} 
-    else
-    {
-		self.zoomSlider.hidden = NO;
-		[self.zoomSlider setValue:_tapZoomImageView.zoomScale animated:YES];
-	}
-	
-	// Rezoom the image to accomodate the presence/absence of the nav bar.
-	[self.tapZoomImageView zoomToCenterWithScale:_tapZoomImageView.zoomScale animated:NO];
-	
-	[self updateZoomSliderUI];
+{
+    // Toggle the nav controller hidden or visible.
+    
+    BOOL shouldHideNavBar = ! [[self navigationController] isNavigationBarHidden];
+    [[self navigationController] setNavigationBarHidden:shouldHideNavBar animated:YES];
+    
+    //  Also Update the zoom slider. Animate if the navbar will be visible.
+    [[self zoomSlider] setHidden: shouldHideNavBar];
+    [[self zoomSlider] setValue:_tapZoomImageView.zoomScale animated: !shouldHideNavBar];
 }
 
-- (void) handleDoubleTap:(id)sender
-{	
-	BOOL isZoomedToOriginalSize = fabs(_tapZoomImageView.zoomScale - 1.0) < 0.0001;
-	
-	if ( isZoomedToOriginalSize ) {
-		// Rezoom to fit the display bounds
-		[self.tapZoomImageView zoomToCenterWithScale: self.tapZoomImageView.zoomToFitScale animated:YES];
-	} else {
-		// Rezoom to the image's original size
-		[self.tapZoomImageView zoomToCenterWithScale:1.0 animated:YES];
-	}
-	
-	// Update the zoom slider
-	[self.zoomSlider setValue:_tapZoomImageView.zoomScale animated:YES];
+- (void) syncZoomSlider:(id)sender
+{
+    [[self zoomSlider] setValue:_tapZoomImageView.zoomScale animated:YES];
 }
-
-- (void) handleTwoFingerTap:(id)sender
-{
-	LOG_ENTRY;
-	
-	// two-finger tap zooms the image back out.
-		
-	// If already at the minimum zoom factor, we're done.
-	BOOL isMinimallyZoomed = fabs(_tapZoomImageView.zoomScale - _tapZoomImageView.minimumZoomScale) < 0.0001;
-	if ( isMinimallyZoomed ) return;
-	
-	// Calculate the new zoom factor, ensuring it stays to the minimum
-	CGFloat newScale = _tapZoomImageView.zoomScale / ZOOM_OUT_FACTOR;
-	
-	if ( newScale < _tapZoomImageView.minimumZoomScale ) {
-		newScale = _tapZoomImageView.minimumZoomScale;
-	}
-	
-	[UIView beginAnimations:nil context:NULL];
-	{
-		// Zoom the image to the tap point at the new scale value.
-		[self.tapZoomImageView zoomToCenterWithScale:newScale animated:NO];
-
-		// Update the zoom slider
-		[self.zoomSlider setValue:_tapZoomImageView.zoomScale animated:NO];
-	}
-	
-	[UIView commitAnimations];
-	
-	LOG_EXIT;
-}
-
-
 @end
 
 

PanZoomImageDemo.xcodeproj/project.pbxproj

 		F234DCE510E9B04D006BF60D /* large.png in Resources */ = {isa = PBXBuildFile; fileRef = F234DCE310E9B04D006BF60D /* large.png */; };
 		F234DCE610E9B04D006BF60D /* small.png in Resources */ = {isa = PBXBuildFile; fileRef = F234DCE410E9B04D006BF60D /* small.png */; };
 		F2517B8110FAC2A4003494E0 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = F2517B8010FAC2A4003494E0 /* LICENSE */; };
-		F256D7CC10FD0152004DE33D /* SOPanZoomImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = F256D7CA10FD0152004DE33D /* SOPanZoomImageView.m */; };
+		F256D7CC10FD0152004DE33D /* SOTapPanZoomImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = F256D7CA10FD0152004DE33D /* SOTapPanZoomImageView.m */; };
 		F256D7D210FD0168004DE33D /* UIResponder+Debugging.m in Sources */ = {isa = PBXBuildFile; fileRef = F256D7D110FD0168004DE33D /* UIResponder+Debugging.m */; };
 		F29C23B910F8275000890669 /* TapZoomDemoViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F29C23B810F8275000890669 /* TapZoomDemoViewController.xib */; };
 /* End PBXBuildFile section */
 		F234DCE310E9B04D006BF60D /* large.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = large.png; sourceTree = "<group>"; };
 		F234DCE410E9B04D006BF60D /* small.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = small.png; sourceTree = "<group>"; };
 		F2517B8010FAC2A4003494E0 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
-		F256D7C910FD0152004DE33D /* SOPanZoomImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SOPanZoomImageView.h; sourceTree = "<group>"; };
-		F256D7CA10FD0152004DE33D /* SOPanZoomImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SOPanZoomImageView.m; sourceTree = "<group>"; };
+		F256D7C910FD0152004DE33D /* SOTapPanZoomImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SOTapPanZoomImageView.h; path = Classes/SOTapPanZoomImageView.h; sourceTree = "<group>"; };
+		F256D7CA10FD0152004DE33D /* SOTapPanZoomImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SOTapPanZoomImageView.m; path = Classes/SOTapPanZoomImageView.m; sourceTree = "<group>"; };
 		F256D7D010FD0168004DE33D /* UIResponder+Debugging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIResponder+Debugging.h"; path = "Classes/UIResponder+Debugging.h"; sourceTree = "<group>"; };
 		F256D7D110FD0168004DE33D /* UIResponder+Debugging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIResponder+Debugging.m"; path = "Classes/UIResponder+Debugging.m"; sourceTree = "<group>"; };
 		F256D7D310FD016E004DE33D /* UsefulMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = UsefulMacros.h; path = Classes/UsefulMacros.h; sourceTree = "<group>"; };
 				F234DBDB10E99B0A006BF60D /* TapZoomDemoViewController.h */,
 				F234DBDC10E99B0A006BF60D /* TapZoomDemoViewController.m */,
 				F29C23B810F8275000890669 /* TapZoomDemoViewController.xib */,
-				F256D7C910FD0152004DE33D /* SOPanZoomImageView.h */,
-				F256D7CA10FD0152004DE33D /* SOPanZoomImageView.m */,
 				28C286DF0D94DF7D0034E888 /* RootViewController.h */,
 				28C286E00D94DF7D0034E888 /* RootViewController.m */,
 				28F335F01007B36200424DE2 /* RootViewController.xib */,
 		29B97314FDCFA39411CA2CEA /* CustomTemplate */ = {
 			isa = PBXGroup;
 			children = (
+				F256D7C910FD0152004DE33D /* SOTapPanZoomImageView.h */,
+				F256D7CA10FD0152004DE33D /* SOTapPanZoomImageView.m */,
 				080E96DDFE201D6D7F000001 /* Classes */,
 				29B97315FDCFA39411CA2CEA /* Other Sources */,
 				29B97317FDCFA39411CA2CEA /* Resources */,
 				1D3623260D0F684500981E51 /* AppDelegate.m in Sources */,
 				28C286E10D94DF7D0034E888 /* RootViewController.m in Sources */,
 				F234DBDE10E99B0A006BF60D /* TapZoomDemoViewController.m in Sources */,
-				F256D7CC10FD0152004DE33D /* SOPanZoomImageView.m in Sources */,
+				F256D7CC10FD0152004DE33D /* SOTapPanZoomImageView.m in Sources */,
 				F256D7D210FD0168004DE33D /* UIResponder+Debugging.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

TapZoomDemoViewController.xib

 <?xml version="1.0" encoding="UTF-8"?>
 <archive type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="7.10">
 	<data>
-		<int key="IBDocument.SystemTarget">1280</int>
+		<int key="IBDocument.SystemTarget">1296</int>
 		<string key="IBDocument.SystemVersion">11D50b</string>
 		<string key="IBDocument.InterfaceBuilderVersion">2182</string>
 		<string key="IBDocument.AppKitVersion">1138.32</string>
 						<int key="NSvFlags">274</int>
 						<string key="NSFrameSize">{320, 416}</string>
 						<reference key="NSSuperview" ref="597637606"/>
+						<reference key="NSNextKeyView" ref="253685308"/>
 						<object class="NSColor" key="IBUIBackgroundColor">
 							<int key="NSColorSpace">1</int>
 							<bytes key="NSRGB">MCAwIDAAA</bytes>
 						</object>
 						<bool key="IBUIClearsContextBeforeDrawing">NO</bool>
+						<int key="IBUIContentMode">1</int>
 						<bool key="IBUIMultipleTouchEnabled">YES</bool>
 						<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
 					</object>
 						<float key="IBUIValue">0.5</float>
 					</object>
 				</object>
-				<string key="NSFrameSize">{320, 416}</string>
+				<string key="NSFrame">{{0, 64}, {320, 416}}</string>
+				<reference key="NSNextKeyView" ref="577061875"/>
 				<object class="NSColor" key="IBUIBackgroundColor">
 					<int key="NSColorSpace">1</int>
 					<bytes key="NSRGB">MSAxIDEAA</bytes>
 						<reference key="source" ref="577061875"/>
 						<reference key="destination" ref="372490531"/>
 					</object>
-					<int key="connectionID">44</int>
+					<int key="connectionID">45</int>
 				</object>
 				<object class="IBConnectionRecord">
 					<object class="IBCocoaTouchEventConnection" key="connection">
 					<string>UIResponder</string>
 					<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
 					<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
-					<string>SOPanZoomImageView</string>
+					<string>SOTapPanZoomImageView</string>
 					<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
 					<object class="NSMutableAttributedString">
 						<object class="NSMutableString" key="NSString">
 				<reference key="dict.values" ref="0"/>
 			</object>
 			<nil key="sourceID"/>
-			<int key="maxID">44</int>
+			<int key="maxID">45</int>
 		</object>
-		<object class="IBClassDescriber" key="IBDocument.Classes"/>
+		<object class="IBClassDescriber" key="IBDocument.Classes">
+			<object class="NSMutableArray" key="referencedPartialClassDescriptions">
+				<bool key="EncodedWithXMLCoder">YES</bool>
+				<object class="IBPartialClassDescription">
+					<string key="className">SOPanZoomImageView</string>
+					<string key="superclassName">UIScrollView</string>
+					<object class="IBClassDescriptionSource" key="sourceIdentifier">
+						<string key="majorKey">IBProjectSource</string>
+						<string key="minorKey">./Classes/SOPanZoomImageView.h</string>
+					</object>
+				</object>
+				<object class="IBPartialClassDescription">
+					<string key="className">TapZoomDemoViewController</string>
+					<string key="superclassName">UIViewController</string>
+					<object class="NSMutableDictionary" key="actions">
+						<string key="NS.key.0">changeZoom:</string>
+						<string key="NS.object.0">id</string>
+					</object>
+					<object class="NSMutableDictionary" key="actionInfosByName">
+						<string key="NS.key.0">changeZoom:</string>
+						<object class="IBActionInfo" key="NS.object.0">
+							<string key="name">changeZoom:</string>
+							<string key="candidateClassName">id</string>
+						</object>
+					</object>
+					<object class="NSMutableDictionary" key="outlets">
+						<bool key="EncodedWithXMLCoder">YES</bool>
+						<object class="NSArray" key="dict.sortedKeys">
+							<bool key="EncodedWithXMLCoder">YES</bool>
+							<string>tapZoomImageView</string>
+							<string>zoomSlider</string>
+						</object>
+						<object class="NSArray" key="dict.values">
+							<bool key="EncodedWithXMLCoder">YES</bool>
+							<string>SOPanZoomImageView</string>
+							<string>UISlider</string>
+						</object>
+					</object>
+					<object class="NSMutableDictionary" key="toOneOutletInfosByName">
+						<bool key="EncodedWithXMLCoder">YES</bool>
+						<object class="NSArray" key="dict.sortedKeys">
+							<bool key="EncodedWithXMLCoder">YES</bool>
+							<string>tapZoomImageView</string>
+							<string>zoomSlider</string>
+						</object>
+						<object class="NSArray" key="dict.values">
+							<bool key="EncodedWithXMLCoder">YES</bool>
+							<object class="IBToOneOutletInfo">
+								<string key="name">tapZoomImageView</string>
+								<string key="candidateClassName">SOPanZoomImageView</string>
+							</object>
+							<object class="IBToOneOutletInfo">
+								<string key="name">zoomSlider</string>
+								<string key="candidateClassName">UISlider</string>
+							</object>
+						</object>
+					</object>
+					<object class="IBClassDescriptionSource" key="sourceIdentifier">
+						<string key="majorKey">IBProjectSource</string>
+						<string key="minorKey">./Classes/TapZoomDemoViewController.h</string>
+					</object>
+				</object>
+			</object>
+		</object>
 		<int key="IBDocument.localizationMode">0</int>
 		<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaTouchFramework</string>
+		<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencyDefaults">
+			<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS</string>
+			<real value="1296" key="NS.object.0"/>
+		</object>
 		<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
 			<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3</string>
 			<integer value="3100" key="NS.object.0"/>