Commits

Bill Garrison committed 6d93cb0

Refactored project.
SOZoomingImageView is the custom UIView that displays a scrollable zoomable image with tap gestures. SOZoomingImageView now includes its own UISlider for changing zoom level.

  • Participants
  • Parent commits 1f736b5

Comments (0)

Files changed (14)

File Classes/DemoViewController.h

+//
+//  ImageViewController.h
+//  ImageViewController
+//
+//  Copyright Standard Orbit Software, LLC 2009. All rights reserved.
+//
+// LICENSE <http://creativecommons.org/licenses/BSD/>
+
+/**
+ 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.
+ 
+ The controller enables touch-interaction with the image view:
+ - When the controller is embedded within a navigation controller, single tap toggles the visibility of the navigation bar.
+ - Double-tap toggles image zooming between the image's original size and a zoom-to-fit-display size.
+ - Two-finger tap will zoom out the image by a stepping factor until the minimum zoom is reached.
+ 
+ When the original image size is larger than the device display, the minimum zoom scale will that value which zooms the image to fit the display.
+ When the image is smaller than the device's display, the minimum zoom scale will be 1.0: the original image size.
+ */
+
+#import <UIKit/UIKit.h>
+#import "SOZoomingImageView.h"
+
+@interface DemoViewController : UIViewController
+{
+}
+
+/** Designated initializer
+ \param image The image to be displayed. Should not be nil.
+ */
+- (id) initWithImage:(UIImage *)image;
+
+
+#pragma mark -
+#pragma mark Properties
+
+/** The Image we're displaying. */
+@property (nonatomic, strong) UIImage *image;
+
+@end
+
+#pragma mark -
+
+/* 
+ LICENSE
+ Copyright (c) 2009, Standard Orbit Software, LLC. All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ 
+ Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ Neither the name of the Standard Orbit Software, LLC. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+

File Classes/DemoViewController.m

+//
+//  ImageViewController.m
+//  ImageViewController
+//
+//  Copyright Standard Orbit Software, LLC 2009. All rights reserved.
+//
+// LICENSE <http://creativecommons.org/licenses/BSD/>
+//
+// $Revision$
+// $Author$
+// $Date$
+
+#define DEBUG_ZOOMING 0 // set to 1 for additional zoom calculation debugging
+#define DEBUG_LAYOUT 0
+
+#define ZOOM_OUT_FACTOR 2
+#define ZOOM_MAX_FACTOR 10
+
+#import "DemoViewController.h"
+#import "SOZoomingImageView.h"
+
+@interface DemoViewController()
+@property (nonatomic, strong) SOZoomingImageView *tapZoomImageView;
+@end
+
+@implementation DemoViewController
+
+#pragma mark -
+#pragma mark Properties
+
+@synthesize tapZoomImageView = _tapZoomImageView;
+@synthesize image = _image;
+
+#pragma mark -
+#pragma mark Initializers
+
+// Designated initializer
+
+- (id) initWithImage:(UIImage *)image;
+{
+	LOG_ENTRY;
+	self = [super initWithNibName:nil bundle:nil];
+	if ( !self ) return nil;
+	
+	_image = image;
+	
+	return self;
+}
+
+- (id) init;
+{
+	return [self initWithImage:nil];
+}
+
+#pragma mark -
+#pragma mark Memory Management
+
+- (void) dealloc 
+{
+    [self viewDidUnload];
+    
+    _image = nil;
+	
+	LOG_EXIT;
+}
+
+- (void) viewDidUnload
+{
+    [super viewDidUnload];
+    
+    _tapZoomImageView = nil;
+}
+
+
+- (void) didReceiveMemoryWarning 
+{
+	// Releases the view if it doesn't have a superview.
+    [super didReceiveMemoryWarning];
+}
+
+#pragma mark -
+#pragma mark UIViewController Overrides
+
+- (void) viewDidLoad
+{
+    [super viewDidLoad];
+    
+	LOG_ENTRY;
+    
+    [[self view] setUserInteractionEnabled:YES];
+    [[self view] setMultipleTouchEnabled:YES];
+    
+    _tapZoomImageView = [[SOZoomingImageView alloc] initWithFrame:[[self view] bounds]];
+    [_tapZoomImageView setBackgroundColor:[UIColor blackColor]];
+    [[self view] addSubview:_tapZoomImageView];
+    
+    /* Add our own actions to the TapPanZoom image view gesture recognizers */
+    [[[self tapZoomImageView] singleTap] addTarget:self action:@selector(handleSingleTap:)];
+}
+
+- (void) viewWillAppear:(BOOL)animated
+{
+	LOG_ENTRY;
+	
+	// Install the image and resize the image view to fit it.
+	[[self.tapZoomImageView imageView] setImage: self.image];
+}
+
+
+- (BOOL) shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
+{
+	return YES;
+}
+
+- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
+{
+    self.tapZoomImageView.zoomSlider.hidden = YES;
+}
+
+- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
+{
+	LOG_ENTRY;
+    
+	if ( !self.navigationController.navigationBarHidden) 
+    {
+		// Restore the zoom slider if we're not hiding
+        self.tapZoomImageView.zoomSlider.hidden = NO;
+	}
+}
+
+#pragma mark -
+#pragma mark Gesture Handlers
+
+- (void) handleSingleTap:(id)sender
+{
+    // Toggle the nav controller hidden or visible.
+    
+    BOOL shouldHideNavBar = ! [[self navigationController] isNavigationBarHidden];
+    
+    [UIView animateWithDuration:0.3 animations:^{
+        [[self navigationController] setNavigationBarHidden:shouldHideNavBar animated:YES];
+        
+        //  Also Update the zoom slider. Animate if the navbar will be visible.
+        [[self.tapZoomImageView zoomSlider] setHidden: shouldHideNavBar];
+    }];
+}
+
+@end
+
+

File Classes/RootViewController.m

 // $Date$
 
 #import "RootViewController.h"
-#import "TapZoomDemoViewController.h"
+#import "DemoViewController.h"
 
 @interface RootViewController()
 @end
 
 - (IBAction) showSmallImageInViewer:(id)sender;
 {
-	TapZoomDemoViewController * imageViewer = [TapZoomDemoViewController new];
+	DemoViewController * imageViewer = [DemoViewController new];
 	imageViewer.image = [UIImage imageNamed:@"small.png"];
 	[self.navigationController pushViewController:imageViewer animated:YES];
 }
 
 - (IBAction) showLargeImageInViewer:(id)sender;
 {
-	id imageViewer = [[TapZoomDemoViewController alloc] initWithImage:[UIImage imageNamed:@"large.png"]];
+	id imageViewer = [[DemoViewController alloc] initWithImage:[UIImage imageNamed:@"large.png"]];
 	[self.navigationController pushViewController:imageViewer animated:YES];
 }
 

File 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;
-

File 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";

File Classes/SOZoomingImageView.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 SOZoomingImageView : UIView <UIScrollViewDelegate>
+{
+}
+
+/** 
+ \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 (readonly, nonatomic) CGFloat zoomToFitScale;
+
+/**
+ \return YES if the scroll view zooming scale is such that the image is zoomed to fit the current device orientation.
+ */
+@property (readonly, nonatomic) BOOL isZoomedToFit;
+
+@property (readonly, nonatomic, strong) IBOutlet UIScrollView *scrollView;
+@property (readonly, nonatomic, strong) IBOutlet UIImageView *imageView;
+@property (readonly, nonatomic, strong) IBOutlet UISlider *zoomSlider;
+
+@property (readonly, nonatomic) UITapGestureRecognizer *singleTap;
+@property (readonly, nonatomic) UITapGestureRecognizer *doubleTap;
+@property (readonly, nonatomic) UITapGestureRecognizer *twoFingerTap;
+
+#pragma mark -
+#pragma mark Zooming and Centering
+
+/**
+ Zoom and center the image view.
+ \param animate YES to animate the zoom and centering.
+ */
+- (void) zoomToCenterWithScale:(CGFloat)scale animated:(BOOL)animate;
+
+/**
+ \return YES if scroll view zoom is at the zoom-to-fit value for the current orientation.
+ */
+- (BOOL) isZoomedToFit;
+
+@end
+
+#pragma mark -
+#pragma mark Constants
+
+extern NSString * const ScrollableImageViewZoomAndCenterAnimation;
+

File Classes/SOZoomingImageView.m

+
+//
+//  Created on 01/06/09.
+//  Copyright Standard Orbit Software, LLC 2009. All rights reserved.
+//	LICENSE <http://creativecommons.org/licenses/BSD/>
+
+#import "SOZoomingImageView.h"
+
+#define ZOOM_MAX_FACTOR 10
+#define ZOOM_OUT_FACTOR 2
+
+@implementation SOZoomingImageView
+
+@synthesize scrollView = _scrollView;
+@synthesize imageView = _imageView;
+@synthesize zoomSlider = _zoomSlider;
+
+@synthesize singleTap = _singleTap;
+@synthesize doubleTap = _doubleTap;
+@synthesize twoFingerTap = _twoFingerTap;
+
+@synthesize zoomToFitScale = _zoomToFitScaleFactor;
+
+- (BOOL) isZoomedToFit;
+{
+	BOOL isZTF = fabs(_scrollView.zoomScale - self.zoomToFitScale) < 0.00001;
+	return isZTF;
+}
+
+#pragma mark -
+#pragma mark Initialization
+
+- (void) commonInit;
+{
+	LOG_ENTRY;
+    [self setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth];
+    
+    _scrollView = [[UIScrollView alloc] initWithFrame:[self bounds]];
+    [_scrollView setUserInteractionEnabled:YES];
+    [_scrollView setMultipleTouchEnabled:YES];
+    [_scrollView setDelegate:self];
+    [_scrollView setAutoresizingMask:UIViewAutoresizingNone];
+    
+    [self addSubview:_scrollView];
+    
+	// Init an image view and install in the scroller
+	{
+		_imageView = [[UIImageView alloc] initWithFrame:[_scrollView bounds]];
+		[_imageView setAutoresizingMask:UIViewAutoresizingNone];
+		[_imageView setUserInteractionEnabled:NO];
+		[_imageView setMultipleTouchEnabled:NO];
+        [_scrollView addSubview:_imageView];
+        [_scrollView setContentSize: _imageView.frame.size];
+	}
+    
+    _zoomSlider = [[UISlider alloc] init];
+    [self addSubview:_zoomSlider];
+    [_zoomSlider addTarget:self action:@selector(changeZoom:) forControlEvents:UIControlEventValueChanged];
+    [_zoomSlider setContinuous:YES];
+    
+    /* Init ZTF scale factor to reasonable default; we'll calculate its actual value when an image has been assigned. */
+	_zoomToFitScaleFactor = 1.0;
+    
+    /* Install tap gesture recognizers */
+    
+    _doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)];
+    [_doubleTap setNumberOfTapsRequired:2];
+    [_scrollView addGestureRecognizer:_doubleTap];
+    
+    _singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)];
+    [_scrollView addGestureRecognizer:_singleTap];
+    [_singleTap requireGestureRecognizerToFail:_doubleTap];
+    
+    _twoFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwoFingerTap:)];
+    [_twoFingerTap setNumberOfTapsRequired:2];
+    [_twoFingerTap setNumberOfTouchesRequired:2];
+    [_scrollView addGestureRecognizer:_twoFingerTap];
+}
+
+- (void) awakeFromNib
+{	
+	LOG_ENTRY;
+	[super awakeFromNib];
+	[self commonInit];
+	
+    //	[self.imageView logResponderChain];
+}
+
+// UIView designated initializer override
+- (id) initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    if (!self) return nil;
+    
+    [self commonInit];
+    return self;
+}
+
+- (id) init;
+{
+	LOG_ENTRY;
+	return [self initWithFrame:(CGRect){.size.height = 100., .size.width = 100.0}];
+}
+
+- (void) dealloc;
+{
+    _singleTap = nil;
+    _doubleTap = nil;
+    _twoFingerTap = nil;
+    
+    _imageView = nil;
+    _scrollView = nil;
+    _zoomSlider = nil;
+    
+    LOG_EXIT;
+}
+
+#pragma mark -
+#pragma mark UIView Overrides
+
+- (void) layoutSubviews
+{
+    LOG_ENTRY;
+    [super layoutSubviews];
+    
+    [_scrollView setFrame:[self bounds]];
+    
+    [_imageView sizeToFit];
+	
+	// Center the image
+	[self configureZoomingScale];
+	[self zoomToCenterWithScale:self.zoomToFitScale animated:YES];
+	
+    // Set contentSize so that scrollers accurately reflect the visible portion of the image.
+    [_scrollView setContentSize: _imageView.frame.size];
+    
+    /* Position the slider centered horizontally and offset from the bottom of the view. */
+    
+    CGRect sliderFrame = [_zoomSlider frame];
+	sliderFrame.origin.y = CGRectGetMaxY([self bounds]) - CGRectGetHeight([_zoomSlider frame]) - 25.0;
+    sliderFrame.size.width = CGRectGetWidth([self bounds]) - 20.0;
+    [_zoomSlider setFrame: sliderFrame];
+    
+    CGPoint sliderCenter = [_zoomSlider center];
+    sliderCenter.x = CGRectGetMidX ([self bounds]);
+    [_zoomSlider setCenter:sliderCenter];
+    
+    
+    LOG_EXIT;
+}
+
+- (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:_scrollView.zoomScale animated:YES];
+	}
+}
+
+
+#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
+{	
+	//	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, [_scrollView contentInset])) 
+    {
+		[_scrollView setContentInset: 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
+	
+	[_scrollView setZoomScale:scale animated:NO];
+	[self updateContentInsetForCentering];
+	
+    [_zoomSlider setValue:scale animated:NO];
+    
+	// If animating, kick off the animations.
+	if (animate) [UIView commitAnimations];
+}
+
+/*
+ Calculate the scroll view zoom scale required to fit the image completely within the scroll view's bounds.
+ This is done on each -layoutSubviews invocation.
+ */
+- (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
+    
+    _scrollView.minimumZoomScale = 1.0;
+    
+	if (_zoomToFitScaleFactor < 1.0)
+    {
+		// For large images, the zoom-to-fit scale value will be the minimum
+		_scrollView.minimumZoomScale = _zoomToFitScaleFactor;
+	}
+    
+	_scrollView.maximumZoomScale = ZOOM_MAX_FACTOR;
+	
+    // Update the scale on the slider
+	[_zoomSlider setMaximumValue: _scrollView.maximumZoomScale];
+    [_zoomSlider setMinimumValue: _scrollView.minimumZoomScale];
+	[_zoomSlider setValue:_scrollView.zoomScale animated:YES];
+    
+	//NSLog(@"After calculation: _zoomToFitScale: %f", _zoomToFitScale);
+}
+
+#pragma mark -
+#pragma mark Properties
+
+#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
+{		
+	[self zoomToCenterWithScale:[_scrollView zoomScale] animated:NO];
+}
+
+- (void) handleDoubleTap:(id)sender
+{	
+	BOOL isZoomedToOriginalSize = fabs([_scrollView 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([_scrollView zoomScale] - [_scrollView minimumZoomScale]) < 0.0001;
+	if ( isMinimallyZoomed ) return;
+	
+	// Calculate the new zoom factor, ensuring it stays to the minimum
+	CGFloat newScale = [_scrollView zoomScale] / ZOOM_OUT_FACTOR;
+	
+	if ( newScale < [_scrollView minimumZoomScale] ) 
+    {
+		newScale = [_scrollView minimumZoomScale];
+	}
+    
+    // Zoom the image to the tap point at the new scale value.
+    [self zoomToCenterWithScale:newScale animated:YES];
+}
+
+#pragma mark -
+#pragma mark Zoom Scaling
+
+- (IBAction) changeZoom:(id)sender;
+{
+	[self zoomToCenterWithScale:[_zoomSlider value] animated:NO];
+}
+
+#pragma mark -
+#pragma mark UIScrollViewDelegate methods
+
+/*
+ UIScrollView implements pinch-to-zoom behavior using two delegate methods: -viewForZoomingInScrollView: and -scrollViewDidEndZooming:withView:atScale:
+ */
+
+- (UIView *) viewForZoomingInScrollView:(UIScrollView *)scrollView 
+{
+    return _imageView;
+}
+
+- (void) scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale 
+{	
+	// Always reset the scroll view content size after zooming, so that scroll bars are rescaled appropriately.
+	[_scrollView setContentSize: view.frame.size];
+	
+	// Re-center the image view
+	[self updateContentInsetForCentering];
+	
+	// Update the zoom slider control after a user has performed a pinch-zoom
+	[_zoomSlider setValue:scale animated:YES];
+    
+	LOG_EXIT;
+}
+
+@end
+
+#pragma mark -
+#pragma mark Constants
+
+NSString * const ScrollableImageViewZoomAndCenterAnimation = @"ZoomAndCenterAnimation";

File Classes/TapZoomDemoViewController.h

-//
-//  ImageViewController.h
-//  ImageViewController
-//
-//  Copyright Standard Orbit Software, LLC 2009. All rights reserved.
-//
-// LICENSE <http://creativecommons.org/licenses/BSD/>
-
-/**
- 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.
- 
- The controller enables touch-interaction with the image view:
- - When the controller is embedded within a navigation controller, single tap toggles the visibility of the navigation bar.
- - Double-tap toggles image zooming between the image's original size and a zoom-to-fit-display size.
- - Two-finger tap will zoom out the image by a stepping factor until the minimum zoom is reached.
- 
- When the original image size is larger than the device display, the minimum zoom scale will that value which zooms the image to fit the display.
- When the image is smaller than the device's display, the minimum zoom scale will be 1.0: the original image size.
- */
-
-#import <UIKit/UIKit.h>
-#import "SOTapPanZoomImageView.h"
-
-@interface TapZoomDemoViewController : UIViewController
-{
-}
-
-/** Designated initializer
- \param image The image to be displayed. Should not be nil.
- */
-- (id) initWithImage:(UIImage *)image;
-
-#pragma mark -
-#pragma mark Zooming and Scaling
-
-/** Action method to change the zoom on the image viewer
- */
-- (IBAction) changeZoom:(id)sender;
-
-#pragma mark -
-#pragma mark Properties
-
-/** The Image we're displaying. */
-@property (nonatomic, strong) UIImage *image;
-
-@end
-
-#pragma mark -
-
-/* 
- LICENSE
- Copyright (c) 2009, Standard Orbit Software, LLC. All rights reserved.
- 
- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- 
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of the Standard Orbit Software, LLC. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-

File Classes/TapZoomDemoViewController.m

-//
-//  ImageViewController.m
-//  ImageViewController
-//
-//  Copyright Standard Orbit Software, LLC 2009. All rights reserved.
-//
-// LICENSE <http://creativecommons.org/licenses/BSD/>
-//
-// $Revision$
-// $Author$
-// $Date$
-
-#define DEBUG_ZOOMING 0 // set to 1 for additional zoom calculation debugging
-#define DEBUG_LAYOUT 0
-
-#define ZOOM_OUT_FACTOR 2
-#define ZOOM_MAX_FACTOR 10
-
-#import "TapZoomDemoViewController.h"
-#import "SOTapPanZoomImageView.h"
-
-@interface TapZoomDemoViewController()
-@property (nonatomic, strong) IBOutlet SOTapPanZoomImageView *tapZoomImageView;
-@property (nonatomic, strong) IBOutlet UISlider *zoomSlider;
-- (void) updateZoomSliderUI;
-@end
-
-@implementation TapZoomDemoViewController
-
-#pragma mark -
-#pragma mark Properties
-
-@synthesize tapZoomImageView = _tapZoomImageView;
-@synthesize zoomSlider = _zoomSlider;
-@synthesize image = _image;
-
-#pragma mark -
-#pragma mark Initializers
-
-// Designated initializer
-
-- (id) initWithImage:(UIImage *)image;
-{
-	LOG_ENTRY;
-	self = [super initWithNibName:nil bundle:nil];
-	if ( !self ) return nil;
-	
-	_image = image;
-	
-	return self;
-}
-
-- (id) init;
-{
-	return [self initWithImage:nil];
-}
-
-#pragma mark -
-#pragma mark Memory Management
-
-- (void) dealloc 
-{
-    [self viewDidUnload];
-    
-    _image = nil;
-	
-	LOG_EXIT;
-}
-
-- (void) viewDidUnload
-{
-    [super viewDidUnload];
-    
-    _tapZoomImageView = nil;
-    _zoomSlider = nil;
-}
-
-
-- (void) didReceiveMemoryWarning 
-{
-	// Releases the view if it doesn't have a superview.
-    [super didReceiveMemoryWarning];
-}
-
-#pragma mark -
-#pragma mark UIViewController Overrides
-
-- (void) viewDidLoad
-{
-    [super viewDidLoad];
-    
-	LOG_ENTRY;
-    
-    [[self view] setUserInteractionEnabled:YES];
-    [[self view] setMultipleTouchEnabled:YES];
-    
-    [[self tapZoomImageView] setIndicatorStyle:UIScrollViewIndicatorStyleWhite];
-    
-    /* 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:)];
-    
-
-#if DEBUG_LAYOUT
-	self.imageView.backgroundColor = [UIColor blueColor];
-	self.view.backgroundColor = [UIColor greenColor];
-#endif
-}
-
-- (void) viewWillAppear:(BOOL)animated
-{
-	LOG_ENTRY;
-	
-	// Install the image and resize the image view to fit it.
-	self.tapZoomImageView.image = self.image;
-    
-	[self updateZoomSliderUI];
-}
-
-
-- (BOOL) shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
-{
-	return YES;
-}
-
-- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
-{
-	self.zoomSlider.hidden = YES;
-}
-
-- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
-{
-	LOG_ENTRY;
-	
-	[self updateZoomSliderUI];
-	
-	if ( !self.navigationController.navigationBarHidden) 
-    {
-		// Restore the zoom slider if we're not hiding
-		self.zoomSlider.hidden = NO;
-	}
-}
-
-#pragma mark -
-#pragma mark Zoom Scaling
-
-- (IBAction) changeZoom:(id)sender;
-{
-	[[self tapZoomImageView] zoomToCenterWithScale:[[self zoomSlider] value] animated:NO];
-}
-
-- (void) updateZoomSliderUI
-{
-	// Reposition the slider
-	
-	CGRect sliderFrame = self.zoomSlider.frame;
-	sliderFrame.origin.y = CGRectGetHeight(self.view.frame) - 25.0;
-	self.zoomSlider.frame = sliderFrame;
-	
-	// Update the scale on the slider
-	self.zoomSlider.maximumValue = self.tapZoomImageView.maximumZoomScale;
-	self.zoomSlider.minimumValue = self.tapZoomImageView.minimumZoomScale;
-	[self.zoomSlider setValue:self.tapZoomImageView.zoomScale animated:YES];
-	
-}
-
-#pragma mark -
-#pragma mark UIScrollViewDelegate methods
-
-/*
- UIScrollView implements pinch-to-zoom behavior using two delegate methods: -viewForZoomingInScrollView: and -scrollViewDidEndZooming:withView:atScale:
- */
-
-- (UIView *) viewForZoomingInScrollView:(UIScrollView *)scrollView 
-{
-	ASSERT( self.tapZoomImageView == scrollView );
-    return [self.tapZoomImageView imageView];
-}
-
-- (void) scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale 
-{	
-	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;
-	
-	// Re-center the image view
-	[self.tapZoomImageView updateContentInsetForCentering];
-	
-	// 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
-
-- (void) handleSingleTap:(id)sender
-{
-    // 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) syncZoomSlider:(id)sender
-{
-    [[self zoomSlider] setValue:_tapZoomImageView.zoomScale animated:YES];
-}
-@end
-
-

File Classes/TouchImageViewer.m.orig

-//
-//  TouchImageViewer.m
-//  TouchImageViewer
-//
-//
-
-// set to 1 for additional zoom calculation debugging
-#define DEBUG_ZOOMING 0
-
-#define ZOOM_OUT_FACTOR 2
-#define ZOOM_MAX_FACTOR 20
-
-#import "TouchImageViewer.h"
-
-@interface TouchImageViewer (UtilityMethods)
-/**
- \internal
- 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;
-
-/**
- \internal 
- \return YES if scroll view zoom is at the zoom-to-fit value for the current orientation.
- */
-- (BOOL) isZoomedToFit;
-
-/**
- \internal
- Center the image within the scroll view bounds.
- Sets content offset and insetRect values to keep the image view centered within the scroll view.
- */
-- (void) centerImageView;
-
-/**
- \internal
- Zoom and center the image view.
- \param animate YES to animate the zoom and centering.
- */
-- (void) zoomToCenterWithScale:(CGFloat)scale animated:(BOOL)animate;
-
-/**
- \internal
- Make our configured image display in the view. Invoked whenever the value of our image property changes.
- */
-- (void) showImage;
-
-/**
- \internal
- Debug method.
- */
-- (void) printDimensionsWithLabel:(NSString *)label;
-
-@end
-
-#pragma mark -
-@implementation TouchImageViewer
-
-#pragma mark -
-#pragma mark Initializers
-
-// Designated initializer
-
-- (id) initWithImage:(UIImage *)image;
-{
-	LOG_ENTRY;
-	self = [self initWithNibName:nil bundle:nil];
-	if ( self ) {
-		_image = [image retain];
-	}
-	return self;
-}
-
-// Default initializer: not useful in this implementation.
-
-- (id) init;
-{
-	NSLog(@"Use the designated initializer: -initWithImage:");
-	[self doesNotRecognizeSelector:_cmd];
-	return nil;
-}
-
-#pragma mark -
-#pragma mark NSObject Overrides
-
-- (void) dealloc 
-{
-	ReleaseAndNil( _image );
-	ReleaseAndNil( _scrollingImageView );
-	
-	LOG_EXIT;
-    [super dealloc];
-}
-
-#pragma mark -
-#pragma mark UIViewController Overrides
-
-/*
-As currently implemented, TouchImageViewer programmatically constructs its root view rather than loading from a nib.
-*/
-- (void) loadView;
-{
-	LOG_ENTRY;
-		
-	_scrollingImageView = [[ScrollableImageView alloc] initWithImage:self.image];
-	_scrollingImageView.delegate = self;
-
-	// Configure the scroll view to be our controller's root view.
-	self.view = _scrollingImageView;
-}
-
-- (void) viewWillAppear:(BOOL)animated;
-{
-	LOG_ENTRY;
-	[super viewWillAppear:animated];
-	
-	// Our view should have a superview by now.  Make sure the scrollview frame fills the bounds of the super view.
-	
-	assert( self.view.superview != nil );
-	
-	self.scrollingImageView.frame = self.view.superview.bounds;
-	
-}
-
-- (void)didReceiveMemoryWarning 
-{
-	// Releases the view if it doesn't have a superview.
-    [super didReceiveMemoryWarning];
-}
-
-- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
-{			
-	// Treat zoom-to-fit scale as special: when user has the image zoomed to fit in one orientation, ensure that it remains zoomed to fit when changing to the new orientation.
-	// Otherwise, use whatever zoom scale the user has choosen.
-	
-	// Capture this condition before we recalculate ZTF.
-	BOOL isZTF = [self.scrollingImageView isZoomedToFit];
-	
-	self.scrollingImageView.contentOffset = CGPointZero;
-	self.scrollingImageView.contentInset = UIEdgeInsetsZero;
-	self.scrollingImageView.contentSize = self.scrollingImageView.imageView.frame.size;
-	
-	// Reconfigure the min, max, and zoom-to-fit zooming scale values to accomodate the change in orientation.
-	[self.scrollingImageView configureZoomingScale];
-	
-	if ( isZTF ) {
-		// Always keep zoomed-to-fit
-		[self.scrollingImageView zoomToCenterWithScale: self.scrollingImageView.zoomToFitScale animated:YES];
-	} else {
-		// Keep the user's current zoom value.
-		[self.scrollingImageView zoomToCenterWithScale:self.scrollingImageView.zoomScale animated:YES];
-	}
-}
-
-- (BOOL) shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
-{
-	return YES;
-}
-
-#pragma mark -
-#pragma mark UIScrollViewDelegate methods
-
-- (UIView *) viewForZoomingInScrollView:(UIScrollView *)scrollView 
-{
-	LOG_ENTRY;
-	ASSERT( self.scrollingImageView.imageView != nil );
-    return self.scrollingImageView.imageView;
-}
-
-- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale; // scale between minimum and maximum. called after any 'bounce' animations
-{
-	LOG_ENTRY;
-	NSLog(@"scrollview: %@; view: %@, scale: %g", scrollView, view, scale);
-}
-
-#pragma mark -
-#pragma mark TapDetectingImageViewDelegate methods
-
-- (void) tapDetectingView:(ScrollableImageView *)view gotSingleTapAtPoint:(CGPoint)tapPoint 
-{
-	LOG_ENTRY;
-#pragma unused(view)
-#pragma unused(tapPoint)
-	
-	// Toggle the nav controller hidden or visible.
-	
-	[UIView beginAnimations:nil context:NULL];
-	{	
-		[self.navigationController setNavigationBarHidden:!self.navigationController.isNavigationBarHidden animated:YES];
-		[self.scrollingImageView zoomToCenterWithScale:self.scrollingImageView.zoomScale animated:YES];
-	}
-	[UIView commitAnimations];
-	
-#if DEBUG_ZOOMING
-	[self printDimensionsWithLabel:@"After single tap"];
-#endif
-	
-}
-
-- (void) tapDetectingView:(ScrollableImageView *)view gotDoubleTapAtPoint:(CGPoint)tapPoint 
-{	
-#pragma unused(view)
-#pragma unused(tapPoint)
-	
-	LOG_ENTRY;
-
-	BOOL isZoomedToOriginalSize = fabs(self.scrollingImageView.zoomScale - 1.0) < 0.0001;
-	
-	if ( isZoomedToOriginalSize ) {
-		// Rezoom to fit the display bounds
-		[self.scrollingImageView zoomToCenterWithScale: self.scrollingImageView.zoomToFitScale animated:YES];
-	} else {
-		// Rezoom to the image's original size
-		[self.scrollingImageView zoomToCenterWithScale:1.0 animated:YES];
-	}
-	
-#if DEBUG_ZOOMING
-	[self printDimensionsWithLabel:@"After double tap"];
-#endif
-	
-}
-
-- (void) tapDetectingView:(ScrollableImageView *)view gotTwoFingerTapAtPoint:(CGPoint)tapPoint 
-{
-#pragma unused(view)
-#pragma unused(tapPoint)
-	
-	LOG_ENTRY;
-
-	// two-finger tap zooms the image back out, but not further than the minimum
-	
-	BOOL isMinimallyZoomed = fabs(self.scrollingImageView.zoomScale - self.scrollingImageView.minimumZoomScale) < 0.0001;
-	if ( isMinimallyZoomed ) return;
-	
-	float newScale = self.scrollingImageView.zoomScale / ZOOM_OUT_FACTOR;
-	
-	if ( newScale < self.scrollingImageView.minimumZoomScale ) {
-		newScale = self.scrollingImageView.minimumZoomScale;
-	}
-	
-	// Zoom the image to center
-	[self.scrollingImageView zoomToCenterWithScale:newScale animated:YES];
-
-}
-
-// Debugging helper
-
-
-#pragma mark -
-#pragma mark Properties
-
-@synthesize image = _image;
-@synthesize scrollingImageView = _scrollingImageView;
-
-@end

File DemoViewController.xib

+<?xml version="1.0" encoding="UTF-8"?>
+<archive type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="7.10">
+	<data>
+		<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>
+		<string key="IBDocument.HIToolboxVersion">568.00</string>
+		<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
+			<string key="NS.key.0">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+			<string key="NS.object.0">1181</string>
+		</object>
+		<object class="NSArray" key="IBDocument.IntegratedClassDependencies">
+			<bool key="EncodedWithXMLCoder">YES</bool>
+			<string>IBProxyObject</string>
+			<string>IBUIView</string>
+		</object>
+		<object class="NSArray" key="IBDocument.PluginDependencies">
+			<bool key="EncodedWithXMLCoder">YES</bool>
+			<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+		</object>
+		<object class="NSMutableDictionary" key="IBDocument.Metadata">
+			<string key="NS.key.0">PluginDependencyRecalculationVersion</string>
+			<integer value="1" key="NS.object.0"/>
+		</object>
+		<object class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
+			<bool key="EncodedWithXMLCoder">YES</bool>
+			<object class="IBProxyObject" id="372490531">
+				<string key="IBProxiedObjectIdentifier">IBFilesOwner</string>
+				<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+			</object>
+			<object class="IBProxyObject" id="975951072">
+				<string key="IBProxiedObjectIdentifier">IBFirstResponder</string>
+				<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+			</object>
+			<object class="IBUIView" id="597637606">
+				<reference key="NSNextResponder"/>
+				<int key="NSvFlags">274</int>
+				<string key="NSFrameSize">{320, 460}</string>
+				<reference key="NSSuperview"/>
+				<reference key="NSWindow"/>
+				<object class="NSColor" key="IBUIBackgroundColor">
+					<int key="NSColorSpace">1</int>
+					<bytes key="NSRGB">MSAxIDEAA</bytes>
+				</object>
+				<bool key="IBUIClearsContextBeforeDrawing">NO</bool>
+				<bool key="IBUIMultipleTouchEnabled">YES</bool>
+				<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+			</object>
+		</object>
+		<object class="IBObjectContainer" key="IBDocument.Objects">
+			<object class="NSMutableArray" key="connectionRecords">
+				<bool key="EncodedWithXMLCoder">YES</bool>
+				<object class="IBConnectionRecord">
+					<object class="IBCocoaTouchOutletConnection" key="connection">
+						<string key="label">view</string>
+						<reference key="source" ref="372490531"/>
+						<reference key="destination" ref="597637606"/>
+					</object>
+					<int key="connectionID">20</int>
+				</object>
+			</object>
+			<object class="IBMutableOrderedSet" key="objectRecords">
+				<object class="NSArray" key="orderedObjects">
+					<bool key="EncodedWithXMLCoder">YES</bool>
+					<object class="IBObjectRecord">
+						<int key="objectID">0</int>
+						<object class="NSArray" key="object" id="0">
+							<bool key="EncodedWithXMLCoder">YES</bool>
+						</object>
+						<reference key="children" ref="1000"/>
+						<nil key="parent"/>
+					</object>
+					<object class="IBObjectRecord">
+						<int key="objectID">-1</int>
+						<reference key="object" ref="372490531"/>
+						<reference key="parent" ref="0"/>
+						<string key="objectName">File's Owner</string>
+					</object>
+					<object class="IBObjectRecord">
+						<int key="objectID">-2</int>
+						<reference key="object" ref="975951072"/>
+						<reference key="parent" ref="0"/>
+					</object>
+					<object class="IBObjectRecord">
+						<int key="objectID">16</int>
+						<reference key="object" ref="597637606"/>
+						<object class="NSMutableArray" key="children">
+							<bool key="EncodedWithXMLCoder">YES</bool>
+						</object>
+						<reference key="parent" ref="0"/>
+					</object>
+				</object>
+			</object>
+			<object class="NSMutableDictionary" key="flattenedProperties">
+				<bool key="EncodedWithXMLCoder">YES</bool>
+				<object class="NSArray" key="dict.sortedKeys">
+					<bool key="EncodedWithXMLCoder">YES</bool>
+					<string>-1.CustomClassName</string>
+					<string>-1.IBPluginDependency</string>
+					<string>-2.CustomClassName</string>
+					<string>-2.IBPluginDependency</string>
+					<string>16.IBPluginDependency</string>
+				</object>
+				<object class="NSArray" key="dict.values">
+					<bool key="EncodedWithXMLCoder">YES</bool>
+					<string>DemoViewController</string>
+					<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+					<string>UIResponder</string>
+					<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+					<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+				</object>
+			</object>
+			<object class="NSMutableDictionary" key="unlocalizedProperties">
+				<bool key="EncodedWithXMLCoder">YES</bool>
+				<reference key="dict.sortedKeys" ref="0"/>
+				<reference key="dict.values" ref="0"/>
+			</object>
+			<nil key="activeLocalization"/>
+			<object class="NSMutableDictionary" key="localizations">
+				<bool key="EncodedWithXMLCoder">YES</bool>
+				<reference key="dict.sortedKeys" ref="0"/>
+				<reference key="dict.values" ref="0"/>
+			</object>
+			<nil key="sourceID"/>
+			<int key="maxID">45</int>
+		</object>
+		<object class="IBClassDescriber" key="IBDocument.Classes">
+			<object class="NSMutableArray" key="referencedPartialClassDescriptions">
+				<bool key="EncodedWithXMLCoder">YES</bool>
+				<object class="IBPartialClassDescription">
+					<string key="className">DemoViewController</string>
+					<string key="superclassName">UIViewController</string>
+					<object class="IBClassDescriptionSource" key="sourceIdentifier">
+						<string key="majorKey">IBProjectSource</string>
+						<string key="minorKey">./Classes/DemoViewController.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"/>
+		</object>
+		<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
+		<int key="IBDocument.defaultPropertyAccessControl">3</int>
+		<string key="IBCocoaTouchPluginVersion">1181</string>
+	</data>
+</archive>

File MainWindow.xib

 <?xml version="1.0" encoding="UTF-8"?>
 <archive type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="7.10">
 	<data>
-		<int key="IBDocument.SystemTarget">784</int>
-		<string key="IBDocument.SystemVersion">10C540</string>
-		<string key="IBDocument.InterfaceBuilderVersion">740</string>
-		<string key="IBDocument.AppKitVersion">1038.25</string>
-		<string key="IBDocument.HIToolboxVersion">458.00</string>
+		<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>
+		<string key="IBDocument.HIToolboxVersion">568.00</string>
 		<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
 			<string key="NS.key.0">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
-			<string key="NS.object.0">62</string>
+			<string key="NS.object.0">1181</string>
 		</object>
-		<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
+		<object class="NSArray" key="IBDocument.IntegratedClassDependencies">
 			<bool key="EncodedWithXMLCoder">YES</bool>
+			<string>IBProxyObject</string>
+			<string>IBUINavigationController</string>
+			<string>IBUIViewController</string>
+			<string>IBUICustomObject</string>
+			<string>IBUIWindow</string>
+			<string>IBUINavigationBar</string>
+			<string>IBUINavigationItem</string>
 		</object>
 		<object class="NSArray" key="IBDocument.PluginDependencies">
 			<bool key="EncodedWithXMLCoder">YES</bool>
 			<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
 		</object>
 		<object class="NSMutableDictionary" key="IBDocument.Metadata">
-			<bool key="EncodedWithXMLCoder">YES</bool>
-			<object class="NSArray" key="dict.sortedKeys" id="0">
-				<bool key="EncodedWithXMLCoder">YES</bool>
-			</object>
-			<object class="NSMutableArray" key="dict.values">
-				<bool key="EncodedWithXMLCoder">YES</bool>
-			</object>
+			<string key="NS.key.0">PluginDependencyRecalculationVersion</string>
+			<integer value="1" key="NS.object.0"/>
 		</object>
 		<object class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
 			<bool key="EncodedWithXMLCoder">YES</bool>
 			<object class="IBProxyObject" id="841351856">
 				<string key="IBProxiedObjectIdentifier">IBFilesOwner</string>
+				<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
 			</object>
 			<object class="IBProxyObject" id="302016328">
 				<string key="IBProxiedObjectIdentifier">IBFirstResponder</string>
+				<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
 			</object>
-			<object class="IBUICustomObject" id="664661524"/>
+			<object class="IBUICustomObject" id="664661524">
+				<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+			</object>
 			<object class="IBUIWindow" id="380026005">
-				<nil key="NSNextResponder"/>
+				<reference key="NSNextResponder"/>
 				<int key="NSvFlags">1316</int>
 				<object class="NSPSMatrix" key="NSFrameMatrix"/>
 				<string key="NSFrameSize">{320, 480}</string>
+				<reference key="NSSuperview"/>
+				<reference key="NSWindow"/>
 				<object class="NSColor" key="IBUIBackgroundColor">
-					<int key="NSColorSpace">1</int>
-					<bytes key="NSRGB">MSAxIDEAA</bytes>
+					<int key="NSColorSpace">3</int>
+					<bytes key="NSWhite">MAA</bytes>
 				</object>
 				<bool key="IBUIOpaque">NO</bool>
 				<bool key="IBUIClearsContextBeforeDrawing">NO</bool>
 				<object class="IBUISimulatedStatusBarMetrics" key="IBUISimulatedStatusBarMetrics"/>
+				<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
 			</object>
 			<object class="IBUINavigationController" id="701001926">
 				<object class="IBUISimulatedNavigationBarMetrics" key="IBUISimulatedTopBarMetrics">
 				<object class="IBUISimulatedStatusBarMetrics" key="IBUISimulatedStatusBarMetrics">
 					<int key="IBUIStatusBarStyle">1</int>
 				</object>
+				<object class="IBUISimulatedOrientationMetrics" key="IBUISimulatedOrientationMetrics">
+					<int key="IBUIInterfaceOrientation">1</int>
+					<int key="interfaceOrientation">1</int>
+				</object>
+				<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+				<bool key="IBUIHorizontal">NO</bool>
 				<object class="IBUINavigationBar" key="IBUINavigationBar" id="207850653">
 					<nil key="NSNextResponder"/>
 					<int key="NSvFlags">256</int>
 					<bool key="IBUIOpaque">NO</bool>
 					<bool key="IBUIClipsSubviews">YES</bool>
 					<bool key="IBUIMultipleTouchEnabled">YES</bool>
+					<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
 					<int key="IBUIBarStyle">1</int>
 				</object>
 				<object class="NSMutableArray" key="IBUIViewControllers">
 					<bool key="EncodedWithXMLCoder">YES</bool>
 					<object class="IBUIViewController" id="619226028">
-						<object class="IBUINavigationItem" key="IBUINavigationItem" id="394667715"/>
+						<object class="IBUINavigationItem" key="IBUINavigationItem" id="394667715">
+							<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+						</object>
 						<reference key="IBUIParentViewController" ref="701001926"/>
 						<string key="IBUINibName">RootViewController</string>
 						<object class="IBUISimulatedStatusBarMetrics" key="IBUISimulatedStatusBarMetrics"/>
+						<object class="IBUISimulatedOrientationMetrics" key="IBUISimulatedOrientationMetrics">
+							<int key="IBUIInterfaceOrientation">1</int>
+							<int key="interfaceOrientation">1</int>
+						</object>
+						<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+						<bool key="IBUIHorizontal">NO</bool>
 					</object>
 				</object>
 			</object>
 					<bool key="EncodedWithXMLCoder">YES</bool>