Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
3_Tiling/Classes/RootViewController.m
/* |
File: RootViewController.m |
Abstract: View controller to manage a scrollview that displays a zoomable image. |
Version: 1.1 |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright (C) 2010 Apple Inc. All Rights Reserved. |
*/ |
#import <QuartzCore/QuartzCore.h> |
#import "RootViewController.h" |
#import "TapDetectingView.h" |
#define ZOOM_VIEW_TAG 100 |
#define ZOOM_STEP 1.5 |
#define THUMB_HEIGHT 150 |
#define THUMB_V_PADDING 10 |
#define THUMB_H_PADDING 10 |
#define CREDIT_LABEL_HEIGHT 20 |
#define AUTOSCROLL_THRESHOLD 30 |
@interface RootViewController (ViewHandlingMethods) |
- (void)toggleThumbView; |
- (void)pickImageNamed:(NSString *)name size:(CGSize)size; |
- (NSArray *)imageData; |
- (void)createThumbScrollViewIfNecessary; |
- (void)createSlideUpViewIfNecessary; |
@end |
@interface RootViewController (AutoscrollingMethods) |
- (void)maybeAutoscrollForThumb:(ThumbImageView *)thumb; |
- (void)autoscrollTimerFired:(NSTimer *)timer; |
- (void)legalizeAutoscrollDistance; |
- (float)autoscrollDistanceForProximityToEdge:(float)proximity; |
@end |
@interface RootViewController (UtilityMethods) |
- (CGRect)zoomRectForScale:(float)scale withCenter:(CGPoint)center; |
@end |
@implementation RootViewController |
- (void)loadView { |
[super loadView]; |
imageScrollView = [[TiledScrollView alloc] initWithFrame:[[self view] bounds]]; |
[imageScrollView setDataSource:self]; |
[[imageScrollView tileContainerView] setDelegate:self]; |
[imageScrollView setTileSize:CGSizeMake(256, 256)]; |
[imageScrollView setBackgroundColor:[UIColor blackColor]]; |
[imageScrollView setBouncesZoom:YES]; |
[imageScrollView setMaximumResolution:0]; |
[imageScrollView setMinimumResolution:-2]; |
[[self view] addSubview:imageScrollView]; |
// we now have to pass the size of the image, because we're not loading the entire image at once |
[self pickImageNamed:@"WeCanDoIt" size:CGSizeMake(1730, 2430)]; |
} |
- (void)dealloc { |
[imageScrollView release]; |
[slideUpView release]; |
[thumbScrollView release]; |
[super dealloc]; |
} |
#pragma mark TiledScrollViewDataSource method |
- (UIView *)tiledScrollView:(TiledScrollView *)tiledScrollView tileForRow:(int)row column:(int)column resolution:(int)resolution { |
// re-use a tile rather than creating a new one, if possible |
UIImageView *tile = (UIImageView *)[tiledScrollView dequeueReusableTile]; |
if (!tile) { |
// the scroll view will handle setting the tile's frame, so we don't have to worry about it |
tile = [[[UIImageView alloc] initWithFrame:CGRectZero] autorelease]; |
// Some of the tiles won't be completely filled, because they're on the right or bottom edge. |
// By default, the image would be stretched to fill the frame of the image view, but we don't |
// want this. Setting the content mode to "top left" ensures that the images around the edge are |
// positioned properly in their tiles. |
[tile setContentMode:UIViewContentModeTopLeft]; |
} |
// The resolution is stored as a power of 2, so -1 means 50%, -2 means 25%, and 0 means 100%. |
// We've named the tiles things like BlackLagoon_50_0_2.png, where the 50 represents 50% resolution. |
int resolutionPercentage = 100 * pow(2, resolution); |
[tile setImage:[UIImage imageNamed:[NSString stringWithFormat:@"%@_%d_%d_%d.png", currentImageName, resolutionPercentage, column, row]]]; |
return tile; |
} |
#pragma mark TapDetectingViewDelegate |
- (void)tapDetectingView:(TapDetectingView *)view gotSingleTapAtPoint:(CGPoint)tapPoint { |
[self toggleThumbView]; |
} |
- (void)tapDetectingView:(TapDetectingView *)view gotDoubleTapAtPoint:(CGPoint)tapPoint { |
// double tap zooms in |
float newScale = [imageScrollView zoomScale] * ZOOM_STEP; |
CGRect zoomRect = [self zoomRectForScale:newScale withCenter:tapPoint]; |
[imageScrollView zoomToRect:zoomRect animated:YES]; |
} |
- (void)tapDetectingView:(TapDetectingView *)view gotTwoFingerTapAtPoint:(CGPoint)tapPoint { |
// two-finger tap zooms out |
float newScale = [imageScrollView zoomScale] / ZOOM_STEP; |
CGRect zoomRect = [self zoomRectForScale:newScale withCenter:tapPoint]; |
[imageScrollView zoomToRect:zoomRect animated:YES]; |
} |
#pragma mark ThumbImageViewDelegate |
/************************************** NOTE **************************************/ |
/* For comments on the ThumbImageViewDelegate methods, please see the Autoscroll */ |
/* project in this sample code suite. */ |
/**********************************************************************************/ |
- (void)thumbImageViewWasTapped:(ThumbImageView *)tiv { |
[self pickImageNamed:[tiv imageName] size:[tiv imageSize]]; |
[self toggleThumbView]; |
} |
- (void)thumbImageViewStartedTracking:(ThumbImageView *)tiv { |
[thumbScrollView bringSubviewToFront:tiv]; |
} |
- (void)thumbImageViewMoved:(ThumbImageView *)draggingThumb { |
[self maybeAutoscrollForThumb:draggingThumb]; |
if (CGRectIntersectsRect([draggingThumb frame], [thumbScrollView bounds])) { |
BOOL draggingRight = [draggingThumb frame].origin.x > [draggingThumb home].origin.x; |
NSMutableArray *thumbsToShift = [[NSMutableArray alloc] init]; |
CGPoint touchLocation = [draggingThumb convertPoint:[draggingThumb touchLocation] toView:thumbScrollView]; |
float minX = draggingRight ? CGRectGetMaxX([draggingThumb home]) : touchLocation.x; |
float maxX = draggingRight ? touchLocation.x : CGRectGetMinX([draggingThumb home]); |
for (ThumbImageView *thumb in [thumbScrollView subviews]) { |
if (thumb == draggingThumb) continue; |
if (! [thumb isMemberOfClass:[ThumbImageView class]]) continue; |
float thumbMidpoint = CGRectGetMidX([thumb home]); |
if (thumbMidpoint >= minX && thumbMidpoint <= maxX) { |
[thumbsToShift addObject:thumb]; |
} |
} |
float otherThumbShift = ([draggingThumb home].size.width + THUMB_H_PADDING) * (draggingRight ? -1 : 1); |
float draggingThumbShift = 0.0; |
for (ThumbImageView *otherThumb in thumbsToShift) { |
CGRect home = [otherThumb home]; |
home.origin.x += otherThumbShift; |
[otherThumb setHome:home]; |
[otherThumb goHome]; |
draggingThumbShift += ([otherThumb frame].size.width + THUMB_H_PADDING) * (draggingRight ? 1 : -1); |
} |
[thumbsToShift release]; |
CGRect home = [draggingThumb home]; |
home.origin.x += draggingThumbShift; |
[draggingThumb setHome:home]; |
} |
} |
- (void)thumbImageViewStoppedTracking:(ThumbImageView *)tiv { |
autoscrollDistance = 0; |
[autoscrollTimer invalidate]; |
autoscrollTimer = nil; |
} |
#pragma mark Autoscrolling |
/************************************** NOTE **************************************/ |
/* For comments on the Autoscrolling methods, please see the Autoscroll project */ |
/* in this sample code suite. */ |
/**********************************************************************************/ |
- (void)maybeAutoscrollForThumb:(ThumbImageView *)thumb { |
autoscrollDistance = 0; |
if (CGRectIntersectsRect([thumb frame], [thumbScrollView bounds])) { |
CGPoint touchLocation = [thumb convertPoint:[thumb touchLocation] toView:thumbScrollView]; |
float distanceFromLeftEdge = touchLocation.x - CGRectGetMinX([thumbScrollView bounds]); |
float distanceFromRightEdge = CGRectGetMaxX([thumbScrollView bounds]) - touchLocation.x; |
if (distanceFromLeftEdge < AUTOSCROLL_THRESHOLD) { |
autoscrollDistance = [self autoscrollDistanceForProximityToEdge:distanceFromLeftEdge] * -1; |
} else if (distanceFromRightEdge < AUTOSCROLL_THRESHOLD) { |
autoscrollDistance = [self autoscrollDistanceForProximityToEdge:distanceFromRightEdge]; |
} |
} |
if (autoscrollDistance == 0) { |
[autoscrollTimer invalidate]; |
autoscrollTimer = nil; |
} |
else if (autoscrollTimer == nil) { |
autoscrollTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60.0) |
target:self |
selector:@selector(autoscrollTimerFired:) |
userInfo:thumb |
repeats:YES]; |
} |
} |
- (float)autoscrollDistanceForProximityToEdge:(float)proximity { |
return ceilf((AUTOSCROLL_THRESHOLD - proximity) / 5.0); |
} |
- (void)legalizeAutoscrollDistance { |
float minimumLegalDistance = [thumbScrollView contentOffset].x * -1; |
float maximumLegalDistance = [thumbScrollView contentSize].width - ([thumbScrollView frame].size.width + [thumbScrollView contentOffset].x); |
autoscrollDistance = MAX(autoscrollDistance, minimumLegalDistance); |
autoscrollDistance = MIN(autoscrollDistance, maximumLegalDistance); |
} |
- (void)autoscrollTimerFired:(NSTimer*)timer { |
[self legalizeAutoscrollDistance]; |
CGPoint contentOffset = [thumbScrollView contentOffset]; |
contentOffset.x += autoscrollDistance; |
[thumbScrollView setContentOffset:contentOffset]; |
ThumbImageView *thumb = (ThumbImageView *)[timer userInfo]; |
[thumb moveByOffset:CGPointMake(autoscrollDistance, 0)]; |
} |
#pragma mark View handling methods |
- (void)pickImageNamed:(NSString *)name size:(CGSize)size { |
[currentImageName release]; |
currentImageName = [name retain]; |
// change the content size and reset the state of the scroll view |
// to avoid interactions with different zoom scales and resolutions. |
[imageScrollView reloadDataWithNewContentSize:size]; |
[imageScrollView setContentOffset:CGPointZero]; |
// choose minimum scale so image width fills screen |
float minScale = [imageScrollView frame].size.width / size.width; |
[imageScrollView setMinimumZoomScale:minScale]; |
[imageScrollView setZoomScale:minScale]; |
} |
- (void)toggleThumbView { |
[self createSlideUpViewIfNecessary]; // no-op if slideUpView has already been created |
CGRect frame = [slideUpView frame]; |
if (thumbViewShowing) { |
frame.origin.y += frame.size.height; |
} else { |
frame.origin.y -= frame.size.height; |
} |
[UIView beginAnimations:nil context:nil]; |
[UIView setAnimationDuration:0.3]; |
[slideUpView setFrame:frame]; |
[UIView commitAnimations]; |
thumbViewShowing = !thumbViewShowing; |
} |
- (NSArray *)imageData { |
// the filenames are stored in a plist in the app bundle, so create array by reading this plist |
NSString *path = [[NSBundle mainBundle] pathForResource:@"ImageData" ofType:@"plist"]; |
NSData *plistData = [NSData dataWithContentsOfFile:path]; |
NSString *error; NSPropertyListFormat format; |
NSArray *imageData = [NSPropertyListSerialization propertyListFromData:plistData |
mutabilityOption:NSPropertyListImmutable |
format:&format |
errorDescription:&error]; |
if (!imageData) { |
NSLog(@"Failed to read image names. Error: %@", error); |
[error release]; |
} |
return imageData; |
} |
- (void)createSlideUpViewIfNecessary { |
if (!slideUpView) { |
// create thumb scroll view |
[self createThumbScrollViewIfNecessary]; |
CGRect bounds = [[self view] bounds]; |
float thumbHeight = [thumbScrollView frame].size.height; |
float labelHeight = CREDIT_LABEL_HEIGHT; |
// create label giving credit for images |
UILabel *creditLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, thumbHeight, bounds.size.width, labelHeight)]; |
[creditLabel setBackgroundColor:[UIColor clearColor]]; |
[creditLabel setTextColor:[UIColor whiteColor]]; |
[creditLabel setFont:[UIFont fontWithName:@"AmericanTypewriter" size:14]]; |
[creditLabel setText:@"images courtesy of the american legion"]; |
[creditLabel setTextAlignment:UITextAlignmentCenter]; |
// create container view that will hold scroll view and label |
CGRect frame = CGRectMake(CGRectGetMinX(bounds), CGRectGetMaxY(bounds), bounds.size.width, thumbHeight + labelHeight); |
slideUpView = [[UIView alloc] initWithFrame:frame]; |
[slideUpView setBackgroundColor:[UIColor blackColor]]; |
[slideUpView setOpaque:NO]; |
[slideUpView setAlpha:0.75]; |
[[self view] addSubview:slideUpView]; |
// add subviews to container view |
[slideUpView addSubview:thumbScrollView]; |
[slideUpView addSubview:creditLabel]; |
[creditLabel release]; |
} |
} |
- (void)createThumbScrollViewIfNecessary { |
if (!thumbScrollView) { |
float scrollViewHeight = THUMB_HEIGHT + THUMB_V_PADDING; |
float scrollViewWidth = [[self view] bounds].size.width; |
thumbScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, scrollViewWidth, scrollViewHeight)]; |
[thumbScrollView setBackgroundColor:nil]; |
[thumbScrollView setCanCancelContentTouches:NO]; |
[thumbScrollView setClipsToBounds:NO]; |
// now place all the thumb views as subviews of the scroll view |
// and in the course of doing so calculate the content width |
float xPosition = THUMB_H_PADDING; |
for (NSDictionary *imageDict in [self imageData]) { |
NSString *name = [imageDict valueForKey:@"name"]; |
UIImage *thumbImage = [UIImage imageNamed:[NSString stringWithFormat:@"%@_thumb.png", name]]; |
if (thumbImage) { |
ThumbImageView *thumbView = [[ThumbImageView alloc] initWithImage:thumbImage]; |
[thumbView setDelegate:self]; |
[thumbView setImageName:name]; |
[thumbView setImageSize:CGSizeMake([[imageDict valueForKey:@"width"] floatValue], [[imageDict valueForKey:@"height"] floatValue])]; |
CGRect frame = [thumbView frame]; |
frame.origin.y = THUMB_V_PADDING; |
frame.origin.x = xPosition; |
[thumbView setFrame:frame]; |
[thumbView setHome:frame]; |
[thumbScrollView addSubview:thumbView]; |
[thumbView release]; |
xPosition += (frame.size.width + THUMB_H_PADDING); |
} |
} |
[thumbScrollView setContentSize:CGSizeMake(xPosition, scrollViewHeight)]; |
} |
} |
- (CGRect)zoomRectForScale:(float)scale withCenter:(CGPoint)center { |
CGRect zoomRect; |
// the zoom rect is in the content view's coordinates. |
// At a zoom scale of 1.0, it would be the size of the imageScrollView's bounds. |
// As the zoom scale decreases, so more content is visible, the size of the rect grows. |
zoomRect.size.height = [imageScrollView frame].size.height / scale; |
zoomRect.size.width = [imageScrollView frame].size.width / scale; |
// choose an origin so as to get the right center. |
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0); |
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0); |
return zoomRect; |
} |
@end |
Copyright © 2010 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2010-10-20