38

I would like to have scroll view with an image content view. The image is actually map which is much bigger than the screen. The map should be initially in the center of the scroll view, like photos in Photos app when you turn iPhone to landscape orientation.

alt text

I did not manage to have the map in the center with correct zooming and scrolling at the same time. Provided that the map image starts from the top of the screen (in portrait orientation), the code looks something like:

- (void)loadView {
    mapView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"map.jpg"]];
    CGFloat mapHeight = MAP_HEIGHT * SCREEN_WIDTH / MAP_WIDTH;
    mapView.frame = CGRectMake(0, 0, SCREEN_WIDTH, mapHeight);
    scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)];
    scrollView.delegate = self;
    scrollView.contentSize = mapView.frame.size;
    scrollView.maximumZoomScale = MAP_WIDTH / SCREEN_WIDTH;
    scrollView.minimumZoomScale = 1;
    [scrollView addSubview:mapView];
    self.view = scrollView;
}

When I move the image frame to the center, the image grows only from the top of its frame down. I tried to play around with mapView transform, with dynamically changing frame of the imageView. Nothing works for me so far.

Community
  • 1
  • 1
Martin Ludvik
  • 391
  • 1
  • 4
  • 5
  • Martin, looks like Shizam has provided a working solution for 3.2+ OSes. Maybe you want to pick his answer as the best one. – Andrey Tarantsov Apr 29 '10 at 23:00
  • Here's the best way to solve this problem: http://blog.proculo.de/archives/180-Paging-enabled-UIScrollView-With-Previews.html It worked very well for me. – ararog Feb 21 '10 at 00:57
  • This is a totally different issue than what is being described here. – Jonah Apr 08 '10 at 03:20

11 Answers11

40

This code should work on most versions of iOS (and has been tested to work on 3.1 upwards).

It's based on the Apple WWDC code mentioned in Jonah's answer.

Add the below to your subclass of UIScrollView, and replace tileContainerView with the view containing your image or tiles:

- (void)layoutSubviews {
    [super layoutSubviews];

    // center the image as it becomes smaller than the size of the screen
    CGSize boundsSize = self.bounds.size;
    CGRect frameToCenter = tileContainerView.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;

    tileContainerView.frame = frameToCenter;
}
JosephH
  • 37,173
  • 19
  • 130
  • 154
  • 7
    This is *by far* the best answer. Clean, simple and it's what Apple suggests in the WWDC 2010 104 session. Please up vote! – Jason Moore Mar 09 '11 at 01:59
  • Warning: this code is NOT perfect - apples rendering on iPad does subview layout slightly slower than their insets handling, so this solution is noticeably less smooth than Shizam's answer. However, Shizam's answer has a small bug where it snaps suddenly if you zoom at the edges. Tested on ipad2 with ios5 – Adam Feb 05 '12 at 17:59
  • 1
    Shizam's answer didn't work for me at all. This did. Although sometimes even this doesn't respond to a Zoom. Pinches always seem to work. – Alyoshak Jun 01 '12 at 16:09
  • I did this code and a few variations. The problem with it is that it occurs after the zoom animation so it causes a "Shaking" effect as the centering lags behind the zoom. – drekka Aug 14 '12 at 02:19
  • How do you hadle rotation? – gyozo kudor Apr 04 '14 at 09:43
  • @gyozokudor layoutSubviews will run after rotation occurs (assuming your view hierarchy/layout/resizemasks are setup correctly). If it's not working for you I suggest posting a new question with more detail as to what your code is doing and what's not working. – JosephH Apr 04 '14 at 09:55
  • 1
    This is absolutely the best answer and solves the entire problem. I just translated this to Xamarin for iOS in about 10 minutes and it works wonderfully. Thanks! – Jesse Feb 20 '15 at 15:19
22

Here is what I'd consider, the solution as in it behaves exactly like apple's photo app. I had been using solutions that used:

-(void) scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale

to recenter but I didn't like that solution because after the zooming was done, it'd bounce then quick 'jump' into the center which was very un-sexy. Turns out if you pretty much do the exact same logic but in this delegate function:

-(void)scrollViewDidZoom:(UIScrollView *)pScrollView

it both starts off centered and when you zoom out it stays centered:

-(void)scrollViewDidZoom:(UIScrollView *)pScrollView {
CGRect innerFrame = imageView.frame;
CGRect scrollerBounds = pScrollView.bounds;

if ( ( innerFrame.size.width < scrollerBounds.size.width ) || ( innerFrame.size.height < scrollerBounds.size.height ) )
{
    CGFloat tempx = imageView.center.x - ( scrollerBounds.size.width / 2 );
    CGFloat tempy = imageView.center.y - ( scrollerBounds.size.height / 2 );
    CGPoint myScrollViewOffset = CGPointMake( tempx, tempy);

    pScrollView.contentOffset = myScrollViewOffset;

}

UIEdgeInsets anEdgeInset = { 0, 0, 0, 0};
if ( scrollerBounds.size.width > innerFrame.size.width )
{
    anEdgeInset.left = (scrollerBounds.size.width - innerFrame.size.width) / 2;
    anEdgeInset.right = -anEdgeInset.left;  // I don't know why this needs to be negative, but that's what works
}
if ( scrollerBounds.size.height > innerFrame.size.height )
{
    anEdgeInset.top = (scrollerBounds.size.height - innerFrame.size.height) / 2;
    anEdgeInset.bottom = -anEdgeInset.top;  // I don't know why this needs to be negative, but that's what works
}
pScrollView.contentInset = anEdgeInset;
}

Where 'imageView' is the UIImageView you're using.

chollida
  • 7,834
  • 11
  • 55
  • 85
Shizam
  • 9,627
  • 8
  • 51
  • 82
  • 2
    Thanks man! This really works (on 3.2+, since scrollViewDidZoom is 3.2-only). For those who will be trying it: contentSize should apparently include contentInset, so just always set contentSize to scrollView.bounds.size. Also I found that setting contentOffset in scrollViewDidZoom is not necessary, and removing it actually helps to avoid some minor jumping. Also imageView.frame is undefined after zooming (although happens to work for now), should replace with imageView.bounds.size * zoomScale. Here's my take on it: http://gist.github.com/384389 – Andrey Tarantsov Apr 29 '10 at 22:42
  • Yes, yes, yes! Brilliant. The real key here is that in 3.2, UIScrollView seems to finally respect contentInset for non-1.0 zoom scales. – Nick Farina Jun 19 '10 at 17:19
  • This is the only real copy&paste-fully-working answer to this problem. Nicely done, Shizam! – gchbib Dec 11 '11 at 15:51
  • Zoom out works perfectly and is very smooth BUT the swap over between zoom in and zoom out has a BUG in this code where it snaps if you pinch quickly. It seems as though the two bits of code have slightly different ways of interpreting the center, and when you zoom slowly across the "zoom=1:1" boundary, animation hides it. Big zooms make it painfully obvious. I havent got a fix for this yet, – Adam Feb 05 '12 at 18:01
  • Anyone experiencing the annoying "jumping bug" that Adam mentioned, try Andrey Tarantsov Gist. – huesforalice Jun 28 '12 at 11:20
  • When the zooming ends, the view "scrolls to top". I do know why? I have tried to set `scrollsToTop` to `NO`. – To1ne Mar 13 '14 at 21:51
  • How do you handle rotation? – gyozo kudor Apr 04 '14 at 09:41
7

Apple has released the 2010 WWDC session videos to all members of the iphone developer program. One of the topics discussed is how they created the photos app!!! They build a very similar app step by step and have made all the code available for free.

It does not use private api either. I can't put any of the code here because of the non disclosure agreement, but here is a link to the sample code download. You will probably need to login to gain access.

http://connect.apple.com/cgi-bin/WebObjects/MemberSite.woa/wa/getSoftware?code=y&source=x&bundleID=20645

And, here is a link to the iTunes WWDC page:

http://insideapple.apple.com/redir/cbx-cgi.do?v=2&la=en&lc=&a=kGSol9sgPHP%2BtlWtLp%2BEP%2FnxnZarjWJglPBZRHd3oDbACudP51JNGS8KlsFgxZto9X%2BTsnqSbeUSWX0doe%2Fzv%2FN5XV55%2FomsyfRgFBysOnIVggO%2Fn2p%2BiweDK%2F%2FmsIXj

Jonah
  • 4,810
  • 14
  • 63
  • 76
  • 1
    Thanks... Took me a second to find it but this is what I needed. It is in the PhotoScroller sample. Look at the ImageScrollView class. – respectTheCode Jul 31 '10 at 11:54
2

I suspect that you need to set the UIScrollView's contentOffset.

etolstoy
  • 1,798
  • 21
  • 33
Rog
  • 17,070
  • 9
  • 50
  • 73
1

I wish it was that simple. I did some research on the net and found that it is not just my problem, but many people are struggling with the same issue not just on iPhone, but on Apple's desktop Cocoa as well. See following links:

http://www.iphonedevsdk.com/forum/iphone-sdk-development/5740-uiimageview-uiscrollview.html
The described solution is based on the property UIViewContentModeScaleAspectFit of the image, but unfortunately it does not work very well .The image is centered and grows properly, but the bouncing area seems to be much bigger than the picture.

This guy did not get the answer either:
http://discussions.apple.com/thread.jspa?messageID=8322675

And finally, the same problem on Apple's desktop Cocoa:
http://www.cocoadev.com/index.pl?CenteringInsideNSScrollView
I suppose the solution works, but it is based on the NSClipView, which is not on iPhone...

Anybody has some solution working on iPhone?

Martin Ludvik
  • 391
  • 1
  • 4
  • 5
1

This worked for me:

- (void) scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
CGFloat tempx = view.center.x-160;
CGFloat tempy = view.center.y-160;
myScrollViewOffset = CGPointMake(tempx,tempy);

}

where 160 is half the width/height of your UIScrollView.

Then later I set the contentoffset to the one captured here.

etolstoy
  • 1,798
  • 21
  • 33
Puppet
  • 13
  • 1
  • 3
0

In Monotouch that worked for me.

this._scroll.ScrollRectToVisible(new RectangleF(_scroll.ContentSize.Width/2, _scroll.ContentSize.Height/2,1,1),false);
ozan.aksu
  • 9
  • 2
0

Note: this method sort of works. if the image is smaller than the imageView, it will scroll partially off the screen. Not a big deal, but also not as nice as the photos app.

First, it's important to understand that we are dealing with 2 views, the imageview with the image in it, and the scrollview with the imageview in it. So, first set the imageview to the size of the screen:

 [myImageView setFrame:self.view.frame];

Then, center your image in the imageview:

 myImageView.contentMode = UIViewContentModeCenter;

Here's my entire code:

- (void)viewDidLoad {
AppDelegate *appDelegate = (pAppDelegate *)[[UIApplication sharedApplication] delegate];
 [super viewDidLoad];
 NSString *Path = [[NSBundle mainBundle] bundlePath];
 NSString *ImagePath = [Path stringByAppendingPathComponent:(@"data: %@", appDelegate.MainImageName)];
 UIImage *tempImg = [[UIImage alloc] initWithContentsOfFile:ImagePath];
 [imgView setImage:tempImg];

 myScrollView = [[UIScrollView alloc] initWithFrame:[[self view] bounds]];
 [myScrollView addSubview:myImageView];

//Set ScrollView Appearance
 [myScrollView setBackgroundColor:[UIColor blackColor]];
 myScrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite;

//Set Scrolling Prefs
 myScrollView.bounces = YES;
 myScrollView.delegate = self;
 myScrollView.clipsToBounds = YES; // default is NO, we want to restrict drawing within our scrollview
 [myScrollView setCanCancelContentTouches:NO];
 [myScrollView setScrollEnabled:YES];


//Set Zooming Prefs
 myScrollView.maximumZoomScale = 3.0;
 myScrollView.minimumZoomScale = CGImageGetWidth(tempImg.CGImage)/320;
 myScrollView.zoomScale = 1.01; //Added the .01 to enable scrolling immediately upon view load.
 myScrollView.bouncesZoom = YES;


 [myImageView setFrame:self.view.frame];//rect];// .frame.size.height = imageHeight;
 myImageView.contentMode = UIViewContentModeCenter;
 self.view = myScrollView;
 [tempImg release];
 }
Jonah
  • 4,810
  • 14
  • 63
  • 76
0

Okay, I think I've found a pretty good solution to this problem. The trick is to constantly readjust the imageView's frame. I find this works much better than constantly adjusting the contentInsets or contentOffSets. I had to add a bit of extra code to accommodate both portrait and landscape images.

Here's the code:

- (void) scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {

CGSize screenSize = [[self view] bounds].size;

if (myScrollView.zoomScale <= initialZoom +0.01) //This resolves a problem with the code not working correctly when zooming all the way out.
{
    imageView.frame = [[self view] bounds];
    [myScrollView setZoomScale:myScrollView.zoomScale +0.01];
}

if (myScrollView.zoomScale > initialZoom)
{
    if (CGImageGetWidth(temporaryImage.CGImage) > CGImageGetHeight(temporaryImage.CGImage)) //If the image is wider than tall, do the following...
    {
            if (screenSize.height >= CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale]) //If the height of the screen is greater than the zoomed height of the image do the following...
            {
                    imageView.frame = CGRectMake(0, 0, 320*(myScrollView.zoomScale), 368);
            }
            if (screenSize.height < CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale]) //If the height of the screen is less than the zoomed height of the image do the following...
            {
                    imageView.frame = CGRectMake(0, 0, 320*(myScrollView.zoomScale), CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale]);
            }
    }
    if (CGImageGetWidth(temporaryImage.CGImage) < CGImageGetHeight(temporaryImage.CGImage)) //If the image is taller than wide, do the following...
    {
            CGFloat portraitHeight;
            if (CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale] < 368)
            { portraitHeight = 368;}
            else {portraitHeight = CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale];}

            if (screenSize.width >= CGImageGetWidth(temporaryImage.CGImage) * [myScrollView zoomScale]) //If the width of the screen is greater than the zoomed width of the image do the following...
            {
                    imageView.frame = CGRectMake(0, 0, 320, portraitHeight);
            }
            if (screenSize.width < CGImageGetWidth (temporaryImage.CGImage) * [myScrollView zoomScale]) //If the width of the screen is less than the zoomed width of the image do the following...
            {
                    imageView.frame = CGRectMake(0, 0, CGImageGetWidth(temporaryImage.CGImage) * [myScrollView zoomScale], portraitHeight);
            }
    }
    [myScrollView setZoomScale:myScrollView.zoomScale -0.01];
}
Jonah
  • 4,810
  • 14
  • 63
  • 76
0

One elegant way to center the content of UISCrollView is this.

Add one observer to the contentSize of your UIScrollView, so this method will be called everytime the content change...

[myScrollView addObserver:delegate 
               forKeyPath:@"contentSize"
                  options:(NSKeyValueObservingOptionNew) 
                  context:NULL];

Now on your observer method:

- (void)observeValueForKeyPath:(NSString *)keyPath   ofObject:(id)object   change:(NSDictionary *)change   context:(void *)context { 

    // Correct Object Class.
    UIScrollView *pointer = object;

    // Calculate Center.
    CGFloat topCorrect = ([pointer bounds].size.height - [pointer viewWithTag:100].bounds.size.height * [pointer zoomScale])  / 2.0 ;
            topCorrect = ( topCorrect < 0.0 ? 0.0 : topCorrect );

    topCorrect = topCorrect - (  pointer.frame.origin.y - imageGallery.frame.origin.y );

    // Apply Correct Center.
    pointer.center = CGPointMake(pointer.center.x,
                                 pointer.center.y + topCorrect ); }
  • You should change the [pointer viewWithTag:100]. Replace by your content view UIView.

    • Also change imageGallery pointing to your window size.

This will correct the center of the content everytime his size change.

NOTE: The only way this content don't works very well is with standard zoom functionality of the UIScrollView.

0

Here's an alternate solution, similar to @JosephH's answer, but this one takes into account the actual dimensions of the image. So that when the user pans/zooms, you never have more whitespace on screen than required. This is a common problem for example when showing a landscape image on a portrait screen. There's going to be whitespace above and below the image when the entire image is on screen (Aspect Fit). Then, when zooming in, the other solutions consider that whitespace as part of the image, because it's in the imageView. They will let you pan the majority of the image off screen, leaving just the whitespace visible. This looks bad for the user.

With this class, you do need to pass it the imageView it's working with. I was tempted to have it auto detect, but this is faster and you want all the speed you can get in the layoutSubviews method.

Note: As is, this requires that AutoLayout is not enabled for the scrollView.

//
//  CentringScrollView.swift
//  Cerebral Gardens
//
//  Created by Dave Wood
//  Copyright © 2016 Cerebral Gardens Inc. All rights reserved.
//

import UIKit

class CentringScrollView: UIScrollView {

    var imageView: UIImageView?

    override func layoutSubviews() {
        super.layoutSubviews()

        guard let superview = superview else { return }
        guard let imageView = imageView else { return }
        guard let image = imageView.image else { return }

        var frameToCentre = imageView.frame

        let imageWidth = image.size.width
        let imageHeight = image.size.height

        let widthRatio = superview.bounds.size.width / imageWidth
        let heightRatio = superview.bounds.size.height / imageHeight

        let minRatio = min(widthRatio, heightRatio, 1.0)

        let effectiveImageWidth = minRatio * imageWidth * zoomScale
        let effectiveImageHeight = minRatio * imageHeight * zoomScale

        contentSize = CGSize(width: max(effectiveImageWidth, bounds.size.width), height: max(effectiveImageHeight, bounds.size.height))

        frameToCentre.origin.x = (contentSize.width - frameToCentre.size.width) / 2
        frameToCentre.origin.y = (contentSize.height - frameToCentre.size.height) / 2

        imageView.frame = frameToCentre
    }
}
Dave Wood
  • 13,143
  • 2
  • 59
  • 67