I have a custom UIViewController onto which I've added a UIImageView (on the StoryBoard) and then embedded the UIImageView in a UIScrollView. My program loads an image into the imageView and scales it to properly fit. By proper fit, I mean this:
if the aspect ratio of the image exactly matches that of the view, the image will be scaled to exactly fit the view, with no scroll bars
otherwise, the image will be scaled to exactly fit in one dimension, with a scrollbar in the other dimension.
I currently do this in viewWillAppear. Below is my code for that method and an auxiliary method that calculates the correct zoomScale:
-(float) findZoomScale {
float widthRatio = self.view.bounds.size.width / self.imageView.image.size.width;
float heightRatio = self.view.bounds.size.height / self.imageView.image.size.height;
float ratio;
if (widthRatio > heightRatio) ratio = widthRatio;
else ratio = heightRatio;
return ratio;
}
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:YES];
self.imageView.image = self.image;
self.imageView.frame = CGRectMake(0, 0, self.imageView.image.size.width, self.imageView.image.size.height);
self.scrollView.contentSize = self.imageView.image.size;
self.scrollView.zoomScale = [self findZoomScale];
[self.scrollView flashScrollIndicators];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:self.photoURL]];
self.imageView.image = self.image;
self.imageView.frame = CGRectMake(0, 0, self.imageView.image.size.width,
self.imageView.image.size.height);
self.scrollView.delegate = self;
self.scrollView.zoomScale = 1.0;
self.navigationItem.title = self.photoTitle;
self.scrollView.autoresizesSubviews = YES;
self.scrollView.autoresizingMask = (UIViewAutoresizingFlexibleBottomMargin |
UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleLeftMargin |
UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight);
}
This code works as I intended for the initial load of an image, whether the iPhone is in landscape or portrait orientation. But when I then rotate the iPhone with the image already loaded, it does not properly rescale. I have tried to do the rescaling in didRotateFromInterfaceOrientation, playing with the scrollView's zoomScale property, but I can't get it to work. Is this the correct place to do the rescaling? And if so, how do I do it? Thanks.
Duane
I just had this exact problem (I assume you're on the CS193p assignment 4 as well), and solved it by setting the zoomScale back to 1 before adjusting the UIScrollView's contentSize upon rotation.
For example:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
//reset zoomScale back to 1 so that contentSize can be modified correctly
self.scrollView.zoomScale = 1;
// reset scrolling area equal to size of image
self.scrollView.contentSize = self.imageView.image.size;
//reset the image frame to the size of the image
self.imageView.frame = CGRectMake(0, 0, self.imageView.image.size.width, self.imageView.image.size.height);
//set the zoomScale to what we actually want
self.scrollView.zoomScale = [self findZoomScale];
}
You can use
-(void)viewWillLayoutSubviews
in your UIViewController. This will get called whenever the view is loaded/rotated/resized.
Related
I am getting a Exc_Bad_Access error in main when I try to scroll my UIScrollView. This program is simple. I have A UIViewController added in AppDelegate. Then, here is the code in the ViewController. This works fine for scrolling but as soon as I add tmpScrollView.delegate = self. The program won't scroll or zoom and gives me the Exc_Bad_Access error when I try to zoom.
#interface MainViewController : UIViewController <UIScrollViewDelegate>
#property (nonatomic,retain) UIScrollView *tmpScrollView;
#property (nonatomic,retain) UIView *containerView;
#end
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
tmpScrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
tmpScrollView.delegate = self;
[self.view addSubview:tmpScrollView];
// Set up the container view to hold our custom view hierarchy
CGSize containerSize = CGSizeMake(640.0f, 640.0f);
self.containerView = [[UIView alloc] initWithFrame:(CGRect){.origin=CGPointMake(0.0f, 0.0f), .size=containerSize}];
[self.tmpScrollView addSubview:self.containerView];
// Set up custom view hierarchy
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 640.0f, 80.0f)];
redView.backgroundColor = [UIColor redColor];
[self.containerView addSubview:redView];
UIView *blueView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 560.0f, 640.0f, 80.0f)];
blueView.backgroundColor = [UIColor blueColor];
[self.containerView addSubview:blueView];
UIView *greenView = [[UIView alloc] initWithFrame:CGRectMake(160.0f, 160.0f, 320.0f, 320.0f)];
greenView.backgroundColor = [UIColor greenColor];
[self.containerView addSubview:greenView];
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"slow.png"]];
imageView.center = CGPointMake(320.0f, 320.0f);
[self.containerView addSubview:imageView];
//Tell the scroll view the size of the contents
self.tmpScrollView.contentSize = containerSize;
}
-(void)viewWillAppear:(BOOL)animated
{
NSLog(#"Will Appear");
[super viewWillAppear:animated];
// Set up the minimum & maximum zoom scales
CGRect scrollViewFrame = self.tmpScrollView.frame;
CGFloat scaleWidth = scrollViewFrame.size.width / self.tmpScrollView.contentSize.width;
CGFloat scaleHeight = scrollViewFrame.size.height / self.tmpScrollView.contentSize.height;
CGFloat minScale = MIN(scaleWidth, scaleHeight);
self.tmpScrollView.minimumZoomScale = minScale;
self.tmpScrollView.maximumZoomScale = 1.0f;
self.tmpScrollView.zoomScale = minScale;
//***** Here is the line for setting the delegate
self.tmpScrollView.delegate = self;
[self centerScrollViewContents];
}
-(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
{
NSLog(#"End Zoom");
}
-(UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView {
// Return the view that we want to zoom
return self.containerView;
}
-(void)scrollViewDidZoom:(UIScrollView *)scrollView {
// The scroll view has zoomed, so we need to re-center the contents
[self centerScrollViewContents];
}
#end
Thanks for any help. This has been driving me crazy for a day now...
Edit: Adding centerScrollViewContens too:
- (void)centerScrollViewContents {
CGSize boundsSize = self.tmpScrollView.bounds.size;
CGRect contentsFrame = self.containerView.frame;
if (contentsFrame.size.width < boundsSize.width) {
contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0f;
} else {
contentsFrame.origin.x = 0.0f;
}
if (contentsFrame.size.height < boundsSize.height) {
contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0f;
} else {
contentsFrame.origin.y = 0.0f;
}
self.containerView.frame = contentsFrame;
}
Whatever Code you have provided is working fine on my end.May be there could be some sort of problem in your "centerScrollViewContents" method.Can you post the code for this method also???
You need to work out the minimum zoom scale for the scroll view. A zoom scale of one means that the content is displayed at normal size. A zoom scale below one shows the content zoomed out, while a zoom scale of greater than one shows the content zoomed in. To get the minimum zoom scale, you calculate how far you’d need to zoom out so that the image fits snugly in your scroll view’s bounds based on its width. Then you do the same based upon the image’s height. The minimum of those two resulting zoom scales will be the scroll view’s minimum zoom scale. That gives you a zoom scale where you can see the entire image when fully zoomed out.
Swift:
let scrollViewFrame = scrollView.frame
let scaleWidth = scrollViewFrame.size.width / scrollView.contentSize.width
let scaleHeight = scrollViewFrame.size.height / scrollView.contentSize.height
let minScale = min(scaleWidth, scaleHeight);
scrollView.minimumZoomScale = minScale;
Objective-C
CGFloat scrollViewFrame = scrollView.frame
CGFloat scaleWidth = scrollViewFrame.size.width / scrollView.contentSize.width
CGFloat scaleHeight = scrollViewFrame.size.height / scrollView.contentSize.height
CGFloat minScale = min(scaleWidth, scaleHeight);
scrollView.minimumZoomScale = minScale;
From other projects and personal experience doing heavy GPU on the iOS (endless scroll, maximum bitmap size, etc), I've noticed that each device has it's own threshold. For past projects, I've also hardcoded the below values as the minimumZoomScale to prevent the crashes:
scale :
{
iPad : {
min : 0.8
, max : 1.6
}
, iPhone : {
min : 0.25
, max : 1.6
}
, iPhone6 : {
min : 0.3333
, max : 1.6
}
, iPhoneResizable: {
min : 1
, max : 1.6
}
}
Great tutorial for beginners: http://www.raywenderlich.com/76436/use-uiscrollview-scroll-zoom-content-swift
I am trying to set up an app very similar to the Photos app on the iPhone. The problem I am running into is that I cannot figure out a good way to set the minimum zoom scale and force the current zoom scale back to the minimum if it is currently smaller than the minimum.
Here is how I am currently setting up my scrollview...
- (void)viewDidLoad{
NSData * imageData = [[[NSData alloc] autorelease] initWithContentsOfURL: [NSURL URLWithString: imageURL]];
UIImage *babe = [UIImage imageWithData:imageData];
babeView = [[UIImageView alloc]
initWithImage:babe];
[self.view addSubview:babeView];
UIBabeScrollView* myScrollview = (UIBabeScrollView*)self.view;
myScrollview.frame = [UIScreen mainScreen].applicationFrame;
[myScrollview setContentSize:[babe size]];
[myScrollview setMaximumZoomScale:2.0];
// Work out a nice minimum zoom for the image - if it's smaller than the ScrollView then 1.0x zoom otherwise a scaled down zoom so it fits in the ScrollView entirely when zoomed out
CGSize imageSize = babeView.image.size;
CGSize scrollSize = myScrollview.frame.size;
CGFloat widthRatio = scrollSize.width / imageSize.width;
CGFloat heightRatio = scrollSize.height / imageSize.height;
CGFloat minimumZoom = MIN(1.0, (widthRatio > heightRatio) ? heightRatio : widthRatio);
[myScrollview setMinimumZoomScale:minimumZoom];
[myScrollview setZoomScale:minimumZoom];
My UIBabeScrollView is subclassed to overload it's layoutSubviews like so...
- (void)layoutSubviews {
[super layoutSubviews];
// center the image as it becomes smaller than the size of the screen
CGSize boundsSize = self.bounds.size;
CGRect frameToCenter = ((UIImageView*)[self.subviews objectAtIndex:0]).frame;
// center horizontally
if (frameToCenter.size.width < boundsSize.width)
frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
else
frameToCenter.origin.x = 0;
// center vertically
if (frameToCenter.size.height < boundsSize.height)
frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
else
frameToCenter.origin.y = 0;
((UIImageView*)[self.subviews objectAtIndex:0]).frame = frameToCenter;
}
The affect I am going for is that the image is always centered, and cannot be zoomed out more than the width or height of the image.
Right now this works fine in portrait mode, but when it is switched to landscape, the zoom scales are incorrect.
Any help would be appreciated, as I am still a fledgling iphone app developer.
In your view controller, implement -willRotateToInterfaceOrientation:duration:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
if (toInterfaceOrientation == UIDeviceOrientationLandscapeLeft ||
toInterfaceOrientation == UIDeviceOrientationLandscapeRight) {
[myScrollview setMinimumZoomScale:yourLandscapeMinimumZoomValue];
[myScrollview setZoomScale:yourLandscapeMinimumZoomValue];
} else {
[myScrollview setMinimumZoomScale:yourPortraitMinimumZoomValue];
[myScrollview setZoomScale:yourPortraitMinimumZoomValue];
}
}
Also another point, just for information. I was trying the [scrollview setZoomScale: :animated] method. On orientation change, it would not Scale to the required value.
It results in an uncompleted animation.
I have a scrollview that automatically generates a series of subviews containing imageviews. The concept is that it's pretty much a custom PDF style reader, where each page is loaded in to an imageview as an image. There are three "layers" - the outer UIScrollView, the automatically generated subViews and the imageview inside the subview.
I've been having some trouble with this, I've asked the question previously, but unfortunately, the core of my question was in the wrong place. Here is my second attempt:
On rotate, everything is rotated as needed. Unfortunately, this is what I'm getting:
Obviously, I would like Image 1 to be centred and for there to be no hint of image 2 until you flick the view across.
Here is my code for setting up the view:
- (void)loadView {
[self setUpView];
}
- (void)setUpView {
//INITIALISE PAGING SCROLL VIEW
CGRect pagingScrollViewFrame = [[UIScreen mainScreen] bounds];
pagingScrollViewFrame.origin.x -= 10;
pagingScrollViewFrame.size.width += 20;
pagingScrollView = [[UIScrollView alloc] initWithFrame:pagingScrollViewFrame];
pagingScrollView.contentMode = UIViewContentModeScaleAspectFit;
//CONFIGURE PAGING SCROLL VIEW
pagingScrollView.pagingEnabled = YES;
pagingScrollView.backgroundColor = [UIColor blackColor];
pagingScrollView.contentSize = CGSizeMake(pagingScrollViewFrame.size.width*7, pagingScrollViewFrame.size.height);
//ACTIVATE PAGING SCROLL VIEW
self.view = pagingScrollView;
//ADD PAGES TO SCROLL VIEW
for (int i = 0; i < 7; i++){
ImageScrollView *page = [[[ImageScrollView alloc] init] autorelease];
[self configurePage:page forIndex:i];
[pagingScrollView addSubview:page];
}
}
How do I re-define the size of the frame? What function should I call etc. How do I centre the image?
I'm new to this so I apologise for the ambiguity of my question.
Cheers
I've figured it out. It was the Frame of the individual pages that I needed to change, so (with the help gained from another question) I wrote a re-orient method.
The first step to this came because I figured out that I needed to iterate through and re-size each frame for each of my pages and then re-apply the frames to the view. For this to happen I needed to create an array which I would fill in the For Loop above. I have 7 total pages.
Then I could (on rotate) call the below re-orient method to re-size each view:
- (void) reOrient{
if(UIInterfaceOrientationIsPortrait(self.interfaceOrientation)){
CGRect f;
int i = 0;
for (ImageScrollView *page in pageArray) {
f = page.frame;
f.size.width = 320;
f.origin.x = (f.size.width * i);
page.frame = f;
pagingScrollView.contentSize = CGSizeMake(f.size.width * 7, 480);
i++;
}
}else{
CGRect f;
int i = 0;
for (ImageScrollView *page in pageArray) {
f = page.frame;
f.size.width = 480;
f.origin.x = (f.size.width * i);
page.frame = f;
pagingScrollView.contentSize = CGSizeMake(f.size.width * 7, 480);
i++;
}
}
}
This worked for me:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
//get current page based on offset before rotation
NSInteger currentPage = self.scrollView.contentOffset.x / self.scrollView.bounds.size.width;
//set anticipated scrollview content size by switching width and height (they will be switched after rotation)
[self.scrollView setContentSize:CGSizeMake(totalPages * self.view.bounds.size.height, self.view.bounds.size.width)];
//set the anticipated content offset based on height before rotation
[self.scrollView setContentOffset:CGPointMake(currentPage * self.scrollView.bounds.size.height, 0.0f)];
}
Basically I am setting the content offset for the new scrollview content size before rotating (I'm switching height and width for the anticipated height and width).
This worked for me when rotating a full-screen paged UIScrollView.
All of the paged subviews have the following autoresizing mask so that they neatly fall into place after rotation:
UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleRightMargin
I have a UIScrollView with a UIImage inside. The following code initializes the image properly (it's width is the same as the width of the scrollView):
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
[super viewDidLoad];
...
imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"map1.jpg"]];
imageView.userInteractionEnabled = YES;
imageView.autoresizingMask = ( UIViewAutoresizingFlexibleWidth );
[imageScrollView addSubview:imageView];
imageScrollView.contentSize = [imageView frame].size;
...
// calculate minimum scale to perfectly fit image width, and begin at that scale
float minimumScale = [imageScrollView frame].size.width / [imageView frame].size.width;
//imageScrollView.maximumZoomScale = 1.0;
imageScrollView.minimumZoomScale = minimumScale;
imageScrollView.zoomScale = minimumScale;
}
What I want to do is to set the size to perfectly fit (like above) when the device rotates. Here's what I've tried to do:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
float newScale = [imageScrollView frame].size.width / [imageView frame].size.width;
imageScrollView.zoomScale = newScale;
}
For some reasons this doesn't work at all. The only way that I was able to make it work was to initialize another UIImage and do all the initialization process again, which doesn't feel right to me.
Probably you should also change .contentSize of scroll view.
I'm ATTEMPTING to learn UIScrollview using Apple's Docs and their sample code http://developer.apple.com/iphone/library/samplecode/Scrolling/index.html but something SO simple is escaping me.
How do you tell what image is currently on the screen, so that if I selected one of the images in the horizontal scrolling view, how would I get the filename of the image, or even a pointer in the array, to then do something further with the image?
I thought with Page Control enable I might be able to find a page # and map it to the image. I thought about counting deceleration to count pages, but a flick no full enough will increment it and give a false number.
The last thing I could think of is to get contentOffSet and divide by image size which will give a 1, 2, 3 and I could point to the array (too tired to try tonight... thought I might ask before I waste a lot more time ;-) ).
Any other ideas? I thought there would be a method somewhere that they use in the photo album app.
PS: Here's the code:
- (void)layoutScrollImages
{
UIImageView *view = nil;
NSArray *subviews = [scrollView1 subviews];
// reposition all image subviews in a horizontal serial fashion
CGFloat curXLoc = 0;
for (view in subviews)
{
if ([view isKindOfClass:[UIImageView class]] && view.tag > 0)
{
CGRect frame = view.frame;
frame.origin = CGPointMake(curXLoc, 0);
view.frame = frame;
curXLoc += (kScrollObjWidth);
}
}
// set the content size so it can be scrollable
[scrollView1 setContentSize:CGSizeMake((kNumImages * kScrollObjWidth), [scrollView1 bounds].size.height)];
}
- (void)viewDidLoad
{
self.view.backgroundColor = [UIColor viewFlipsideBackgroundColor];
// 1. setup the scrollview for multiple images and add it to the view controller
//
// note: the following can be done in Interface Builder, but we show this in code for clarity
[scrollView1 setBackgroundColor:[UIColor blackColor]];
[scrollView1 setCanCancelContentTouches:NO];
scrollView1.indicatorStyle = UIScrollViewIndicatorStyleWhite;
scrollView1.clipsToBounds = YES; // default is NO, we want to restrict drawing within our scrollview
scrollView1.scrollEnabled = YES;
// pagingEnabled property default is NO, if set the scroller will stop or snap at each photo
// if you want free-flowing scroll, don't set this property.
scrollView1.pagingEnabled = YES;
// load all the images from our bundle and add them to the scroll view
//NSUInteger i;
for (i = 1; i <= kNumImages; i++)
{
NSString *imageName = [NSString stringWithFormat:#"Card %d.png", i];
UIImage *image = [UIImage imageNamed:imageName];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
// setup each frame to a default height and width, it will be properly placed when we call "updateScrollList"
CGRect rect = imageView.frame;
rect.size.height = kScrollObjHeight;
rect.size.width = kScrollObjWidth;
imageView.frame = rect;
imageView.tag = i; // tag our images for later use when we place them in serial fashion
[scrollView1 addSubview:imageView];
[imageView release];
}
[self layoutScrollImages]; // now place the photos in serial layout within the scrollview
This was easy after a good sleep!
CGPoint p = scrollView1.contentOffset;
NSLog(#"x = %f, y = %f", p.x, p.y);
Now just divide by 320 (if horizontal and full screen image) and add 1 (because it starts at 0).
Hope this helps someone else!
Paul