144

I have a UIImageView inside a UIScrollView which I use for zooming and scrolling. If the image / content of the scroll view is bigger than the scroll view, everything works fine. However, when the image becomes smaller than the scroll view, it sticks to the top left corner of the scroll view. I would like to keep it centered, like the Photos app.

Any ideas or examples about keeping the content of the UIScrollView centered when it's smaller?

I am working with iPhone 3.0.

The following code almost works. The image returns to the top left corner if I pinch it after reaching the minimum zoom level.

- (void)loadView {
    [super loadView];

    // set up main scroll view
    imageScrollView = [[UIScrollView alloc] initWithFrame:[[self view] bounds]];
    [imageScrollView setBackgroundColor:[UIColor blackColor]];
    [imageScrollView setDelegate:self];
    [imageScrollView setBouncesZoom:YES];
    [[self view] addSubview:imageScrollView];

    UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"WeCanDoIt.png"]];
    [imageView setTag:ZOOM_VIEW_TAG];
    [imageScrollView setContentSize:[imageView frame].size];
    [imageScrollView addSubview:imageView];

    CGSize imageSize = imageView.image.size;
    [imageView release];

    CGSize maxSize = imageScrollView.frame.size;
    CGFloat widthRatio = maxSize.width / imageSize.width;
    CGFloat heightRatio = maxSize.height / imageSize.height;
    CGFloat initialZoom = (widthRatio > heightRatio) ? heightRatio : widthRatio;

    [imageScrollView setMinimumZoomScale:initialZoom];
    [imageScrollView setZoomScale:1];

    float topInset = (maxSize.height - imageSize.height) / 2.0;
    float sideInset = (maxSize.width - imageSize.width) / 2.0;
    if (topInset < 0.0) topInset = 0.0;
    if (sideInset < 0.0) sideInset = 0.0;
    [imageScrollView setContentInset:UIEdgeInsetsMake(topInset, sideInset, -topInset, -sideInset)];
}

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    return [imageScrollView viewWithTag:ZOOM_VIEW_TAG];
}

/************************************** NOTE **************************************/
/* The following delegate method works around a known bug in zoomToRect:animated: */
/* In the next release after 3.0 this workaround will no longer be necessary      */
/**********************************************************************************/
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
    [scrollView setZoomScale:scale+0.01 animated:NO];
    [scrollView setZoomScale:scale animated:NO];
    // END Bug workaround

    CGSize maxSize = imageScrollView.frame.size;
    CGSize viewSize = view.frame.size;
    float topInset = (maxSize.height - viewSize.height) / 2.0;
    float sideInset = (maxSize.width - viewSize.width) / 2.0;
    if (topInset < 0.0) topInset = 0.0;
    if (sideInset < 0.0) sideInset = 0.0;
    [imageScrollView setContentInset:UIEdgeInsetsMake(topInset, sideInset, -topInset, -sideInset)];
}
Pang
  • 8,605
  • 144
  • 77
  • 113
hpique
  • 112,774
  • 126
  • 328
  • 461
  • Did you ever solve this problem completely? I'm struggling with the same issue. – Jonah Sep 19 '09 at 20:03
  • 1
    Attention: Use the initialZoom value to calculate the inset if it is NOT one (no zooming). E.g. use these lines: float topInset = (maxSize.height - imageSize.height * initialZoom) / 2.0; float sideInset = (maxSize.width - imageSize.width * initialZoom) / 2.0; and finally set the initial zoom value [imageScrollView setZoomScale: initialZoom]; – Felix Sep 17 '10 at 12:49

26 Answers26

232

I've got very simple solution! All you need is to update the center of your subview (imageview) while zooming in the ScrollViewDelegate. If zoomed image is smaller than scrollview then adjust subview.center else center is (0,0).

- (void)scrollViewDidZoom:(UIScrollView *)scrollView 
{
    UIView *subView = [scrollView.subviews objectAtIndex:0];

    CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
    CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);

    subView.center = CGPointMake(scrollView.contentSize.width * 0.5 + offsetX, 
                                 scrollView.contentSize.height * 0.5 + offsetY);
}
Erdemus
  • 2,718
  • 2
  • 15
  • 10
  • 1
    Very simple and easy to use! Thx! – Krumelur Mar 08 '11 at 13:07
  • 1
    This works somewhat better than the contentInset adjustment one, at least, for the usages I have tried. The contentInset adjustment option is quite jerky, but this is extremely smooth. – mxcl Mar 19 '11 at 14:09
  • 5
    For me this solution is more jerky than using Liam's NYOBetterZoom. Maybe it depend on image size etc. The moral; use the solution that best suits your needs – wuf810 Apr 15 '11 at 10:36
  • 1
    I wish I could give this answer more than one upvote. SIMPLE, AND EFFECTIVE. – Kenny Winker Aug 21 '11 at 01:55
  • 1
    Dude, you rock!!! I had already went through calculating the ratio between my scrollView and my imageView and had everything working but this.. awesome! saved me coding additional calculations! – geekinit Nov 28 '11 at 21:04
  • 1
    Of all the "solutions" I've tried for this problem, this is the only one I can get to work properly and it's sooo much simpler to implement than some of the others I've seen out there. Thanks a lot for this time saver! – ozz Jan 03 '12 at 04:20
  • 2
    Stackoverflow GOLD STAR for this. I was struggling with this, yet the solution is so simple. – n13 Mar 01 '13 at 07:35
  • 2
    to simplify a bit: `CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);` – Marek R Jan 14 '14 at 14:39
  • 6
    This adjustment helped me out when the scrollview had insets: `CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentInset.left - scrollView.contentInset.right - scrollView.contentSize.width) * 0.5, 0.0); CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentInset.top - scrollView.contentInset.bottom - scrollView.contentSize.height) * 0.5, 0.0);` – Kieran Harper Jun 21 '14 at 00:44
  • 3
    I noticed that this, like many other centering techniques, seems to suffer from problems when using `zoomToRect:`. Using the `contentInset` approach works better, if you happen to need that functionality. See http://petersteinberger.com/blog/2013/how-to-center-uiscrollview/ for more details. – Matej Bukovinski Jan 26 '15 at 16:15
  • This actually also helps keeping the image in the right spot when zooming in, which was a bigger problem in my particular case – Julian F. Weinert Jun 30 '16 at 10:44
54

@EvelynCordner's answer was the one that worked best in my app. A lot less code than the other options too.

Here's the Swift version if anyone needs it:

func scrollViewDidZoom(_ scrollView: UIScrollView) {
    let offsetX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
    let offsetY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
    scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0, 0)
}
xaphod
  • 5,475
  • 1
  • 30
  • 40
William T.
  • 12,040
  • 3
  • 52
  • 51
26

Okay, I've been fighting this for the past two days on and off and having finally come to a pretty reliable (so far...) solution I thought I should share it and save others some pain. :) If you do find a problem with this solution please shout!

I've basically gone through what everyone else has: searching StackOverflow, the Apple Developer Forums, looked at the code for three20, ScrollingMadness, ScrollTestSuite, etc. I've tried enlarging the UIImageView frame, playing with the UIScrollView's offset and/or insets from the ViewController, etc. but nothing worked great (as everyone else has found out too).

After sleeping on it, I tried a couple of alternative angles:

  1. Subclassing the UIImageView so it alters it's own size dynamically - this didn't work well at all.
  2. Subclassing the UIScrollView so it alters it's own contentOffset dynamically - this is the one that seems to be a winner for me.

With this subclassing UIScrollView method I'm overriding the contentOffset mutator so it isn't setting {0,0} when the image is scaled smaller than the viewport - instead it's setting the offset such that the image will be kept centred in the viewport. So far, it always seems to work. I've checked it with wide, tall, tiny & large images and doesn't have the "works but pinch at minimum zoom breaks it" issue.

I've uploaded an example project to github that uses this solution, you can find it here: http://github.com/nyoron/NYOBetterZoom

Liam Jones
  • 311
  • 3
  • 3
  • 2
    For those interested - I've updated the above linked project so it's a bit less reliant on the ViewController doing 'the right thing', the custom UIScrollView itself takes care of more of the detail. – Liam Jones Apr 24 '10 at 12:41
  • 2
    Liam, you rock. I'm saying this as an author of ScrollingMadness. BTW on iPad in 3.2+ setting contentInset in scrollViewDidZoom (a new 3.2+ delegate method) Just Works. – Andrey Tarantsov May 05 '10 at 01:36
  • Very neat piece of code. I've been scratching my head with this for a while. Added the project to http://handyiphonecode.com – samvermette Jun 04 '10 at 17:23
  • This is great. Thank you. It wasn't compensating for my UIStatusBar being hidden though, so I changed the line `anOffset.y = -(scrollViewSize.height - zoomViewSize.height) / 2.0` to `anOffset.y = (-(scrollViewSize.height - zoomViewSize.height) / 2.0) + 10;` – Gordon Fontenot Jun 25 '10 at 18:03
  • The solution given bi Erdemus is much more simple in my opinion. – gyozo kudor Sep 02 '10 at 09:05
  • @LiamJones you don't have a license in your GitHub repo? That ways it's All Rights Reserved. Would you open it up to MIT or something similar? – Julian F. Weinert Jun 30 '16 at 10:32
  • apparently @LiamJones is not on SO anymore, with just this answer. Still i say, you've done and shared great work with that scrollView - that still helps people after 7 years, and works flawlessly. Applauding. – user1244109 Jan 29 '18 at 09:23
24

For a solution better suited for scroll views that use autolayout, use content insets of the scroll view rather than updating the frames of your scroll view's subviews.

- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
    CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
    CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);

    self.scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0.f, 0.f);
}
Evelyn Cordner
  • 241
  • 2
  • 3
  • 1
    This worked for me, however if the image is smaller to begin with somehow this code (when called directly) didn't update the scrollview. What I needed todo was first put the `UIView` that is inside the `UIScrollview` to the same center (`view.center = scrollview.center;`) and then in the `scrollViewDidZoom` set the `view.frame.origin` `x` and `y` to `0` again. – morksinaanab Apr 14 '16 at 07:18
  • Works well for me as well, but I did a `dispatch_async` to the main queue in `viewWillAppear` where I call `scrollViewDidZoom:` on my main scroll view. This makes the view appear with centered images. – Pascal May 16 '16 at 11:17
  • 1
    Place a call to this code in `viewDidLayoutSubviews` to make sure it is properly set up when the view shows the first time. – Carter Medlin Jul 08 '16 at 19:34
23

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 for the photoscoller.

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
  • 36,107
  • 19
  • 126
  • 149
  • 3
    this should be the accepted answer, cause it is simpler. Thanks. You saved my life!!!!!! :D – Duck Nov 24 '10 at 17:44
  • After removing all the iOS 3.2+ API calls, the centering logic only seems to work on devices with iOS 3.2+ and not 3.1.3 (jumpy, flickery, gets random offset). I've compared the outputs of frame origin and size between the 3.1.3 and 3.2+ and even though they do match, for some reason, the child view still gets positioned incorrectly. Very odd. Only Liam Jone's answer worked out for me. – David H Mar 17 '11 at 23:16
  • 1
    Both @Erdemus and @JosephH solutions work, but the `UIScrollView` subclass approach seems preferable: it is invoked when the view is first displayed + continuously while zooming is in progress (whereas `scrollViewDidZoom` is only invoked once per scroll and after the fact) – SwiftArchitect Mar 14 '12 at 03:33
21

Currently I'm subclassing UIScrollView and overriding setContentOffset: to adjust the offset based on contentSize. It works both with pinch and programatic zooming.

@implementation HPCenteringScrollView

- (void)setContentOffset:(CGPoint)contentOffset
{
    const CGSize contentSize = self.contentSize;
    const CGSize scrollViewSize = self.bounds.size;

    if (contentSize.width < scrollViewSize.width)
    {
        contentOffset.x = -(scrollViewSize.width - contentSize.width) / 2.0;
    }

    if (contentSize.height < scrollViewSize.height)
    {
        contentOffset.y = -(scrollViewSize.height - contentSize.height) / 2.0;
    }

    [super setContentOffset:contentOffset];
}

@end

In addition to being short and sweet, this code produces a much smoother zoom than @Erdemus solution. You can see it in action in the RMGallery demo.

hpique
  • 112,774
  • 126
  • 328
  • 461
  • 6
    You don't need to subclass UIScrollView to implement this method. Apple allow you to see scroll and zoom events in the delegate methods. Specifically: - (void)scrollViewDidZoom:(UIScrollView *)scrollView; – Rog Dec 04 '14 at 11:55
  • 1
    Best solution I’ve seen yet (work even when using constraints). – Frizlab Apr 27 '15 at 14:33
12

I've spent a day fighting with this issue, and ended up implementing the scrollViewDidEndZooming:withView:atScale: as follows:

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
    CGFloat screenWidth = [[UIScreen mainScreen] bounds].size.width;
    CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
    CGFloat viewWidth = view.frame.size.width;
    CGFloat viewHeight = view.frame.size.height;

    CGFloat x = 0;
    CGFloat y = 0;

    if(viewWidth < screenWidth) {
        x = screenWidth / 2;
    }
    if(viewHeight < screenHeight) {
        y = screenHeight / 2 ;
    }

    self.scrollView.contentInset = UIEdgeInsetsMake(y, x, y, x);
}

This ensures that when the image is smaller than the screen, there's still adequate space around it so you can position it to the exact place you want.

(assuming that your UIScrollView contains an UIImageView to hold the image)

Essentially, what this does is check whether your image view's width / height is smaller that the screen's width / height, and if so, create an inset of half the screen's width / height (you could probably make this larger if you want the image to go out of the screen bounds).

Note that since this is a UIScrollViewDelegate method, don't forget to add it to your view controller's declaration, so to avoid getting a build issue.

mwright
  • 3,883
  • 6
  • 28
  • 37
Vicarius
  • 294
  • 3
  • 7
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,770
  • 13
  • 60
  • 75
4

If contentInset is not needed for anything else, it can be used to center scrollview's content.

class ContentCenteringScrollView: UIScrollView {

    override var bounds: CGRect {
        didSet { updateContentInset() }
    }

    override var contentSize: CGSize {
        didSet { updateContentInset() }
    }

    private func updateContentInset() {
        var top = CGFloat(0)
        var left = CGFloat(0)
        if contentSize.width < bounds.width {
            left = (bounds.width - contentSize.width) / 2
        }
        if contentSize.height < bounds.height {
            top = (bounds.height - contentSize.height) / 2
        }
        contentInset = UIEdgeInsets(top: top, left: left, bottom: top, right: left)
    }
}

Advantage if this approach is that you can still use contentLayoutGuide to place content inside scrollview

scrollView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    imageView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
    imageView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
    imageView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
    imageView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor)
])

or just drag and drop the content in Xcode's Interface Builder.

Wojciech Nagrodzki
  • 2,508
  • 12
  • 11
2

The way I've done this is to add an extra view into the hierarchy:

UIScrollView -> UIView -> UIImageView

Give your UIView the same aspect ratio as your UIScrollView, and centre your UIImageView into that.

etolstoy
  • 1,788
  • 20
  • 33
hatfinch
  • 3,056
  • 21
  • 35
  • Thanks, hatfinch. Will try this as well. Can you post sample code or change my sample code to show how you construct the view hierarchy? – hpique Aug 24 '09 at 19:42
  • 1
    This sort of works. Except for the fact that because the UIView is the size of the UIScrollView, if the image is smaller (i.e. landscape instead of portrait) you can scroll part of the image off the screen. The photos app does not allow this and looks much nicer. – Jonah Sep 19 '09 at 18:41
2

Ok, this solution is working for me. I have a subclass of UIScrollView with a reference to the UIImageView it is displaying. Whenever the UIScrollView zooms, the contentSize property is adjusted. It is in the setter that I scale the UIImageView appropriately and also adjust its center position.

-(void) setContentSize:(CGSize) size{
CGSize lSelfSize = self.frame.size;
CGPoint mid;
if(self.zoomScale >= self.minimumZoomScale){
    CGSize lImageSize = cachedImageView.initialSize;
    float newHeight = lImageSize.height * self.zoomScale;

    if (newHeight < lSelfSize.height ) {
        newHeight = lSelfSize.height;
    }
    size.height = newHeight;

    float newWidth = lImageSize.width * self.zoomScale;
    if (newWidth < lSelfSize.width ) {
        newWidth = lSelfSize.width;
    }
    size.width = newWidth;
    mid = CGPointMake(size.width/2, size.height/2);

}
else {
    mid = CGPointMake(lSelfSize.width/2, lSelfSize.height/2);
}

cachedImageView.center = mid;
[super  setContentSize:size];
[self printLocations];
NSLog(@"zoom %f setting size %f x %f",self.zoomScale,size.width,size.height);
}

Evertime I set the image on the UIScrollView I resize it. The UIScrollView in the scrollview is also a custom class I created.

-(void) resetSize{
    if (!scrollView){//scroll view is view containing imageview
        return;
    }

    CGSize lSize = scrollView.frame.size;

    CGSize lSelfSize = self.image.size; 
    float lWidth = lSize.width/lSelfSize.width;
    float lHeight = lSize.height/lSelfSize.height;

    // choose minimum scale so image width fits screen
    float factor  = (lWidth<lHeight)?lWidth:lHeight;

    initialSize.height = lSelfSize.height  * factor;
    initialSize.width = lSelfSize.width  * factor;

    [scrollView setContentSize:lSize];
    [scrollView setContentOffset:CGPointZero];
    scrollView.userInteractionEnabled = YES;
}

With these two methods I am able to have a view that behaves just like the photos app.

DShah
  • 9,510
  • 10
  • 66
  • 123
AHA
  • 21
  • 3
  • This seems to do the trick, and it's a fairly simple solution. Some zooming transitions are not perfect, but I believe it can be fixed. I will experiment some more. – hpique Apr 07 '10 at 11:42
  • The solution was clear before the update. Now it's a bit confusing. Can you clarify? – hpique Apr 28 '10 at 09:36
2

Just the approved answer in swift, but without subclassing using the delegate

func centerScrollViewContents(scrollView: UIScrollView) {
    let contentSize = scrollView.contentSize
    let scrollViewSize = scrollView.frame.size;
    var contentOffset = scrollView.contentOffset;

    if (contentSize.width < scrollViewSize.width) {
        contentOffset.x = -(scrollViewSize.width - contentSize.width) / 2.0
    }

    if (contentSize.height < scrollViewSize.height) {
        contentOffset.y = -(scrollViewSize.height - contentSize.height) / 2.0
    }

    scrollView.setContentOffset(contentOffset, animated: false)
}

// UIScrollViewDelegate    
func scrollViewDidZoom(scrollView: UIScrollView) {
    centerScrollViewContents(scrollView)
}
LightMan
  • 3,068
  • 26
  • 26
2

I know some answers above are right, but I just want to give my answer with some explanation, the comments will make you understand why we do like this.

When I load the scrollView for the first time, I write the following code to make it center, please notice we set contentOffset first, then contentInset

    scrollView.maximumZoomScale = 8
    scrollView.minimumZoomScale = 1

    // set vContent frame
    vContent.frame = CGRect(x: 0,
                            y: 0  ,
                            width: vContentWidth,
                            height: vContentWidth)
    // set scrollView.contentSize
    scrollView.contentSize = vContent.frame.size

    //on the X direction, if contentSize.width > scrollView.bounds.with, move scrollView from 0 to offsetX to make it center(using `scrollView.contentOffset`)
    // if not, don't need to set offset, but we need to set contentInset to make it center.(using `scrollView.contentInset`)
    // so does the Y direction.
    let offsetX = max((scrollView.contentSize.width - scrollView.bounds.width) * 0.5, 0)
    let offsetY = max((scrollView.contentSize.height - scrollView.bounds.height) * 0.5, 0)
    scrollView.contentOffset = CGPoint(x: offsetX, y: offsetY)

    let topX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
    let topY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
    scrollView.contentInset = UIEdgeInsets(top: topY, left: topX, bottom: 0, right: 0)

Then, when I pinch vContent, I write the following code to make it center.

func scrollViewDidZoom(_ scrollView: UIScrollView) {
    //we just need to ensure that the content is in the center when the contentSize is less than scrollView.size.
    let topX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
    let topY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
    scrollView.contentInset = UIEdgeInsets(top: topY, left: topX, bottom: 0, right: 0)
}
Changwei
  • 644
  • 1
  • 5
  • 16
1

You could watch the contentSize property of the UIScrollView (using key-value observing or similar), and automatically adjust the contentInset whenever the contentSize changes to be less than the size of the scroll view.

Tim
  • 57,767
  • 18
  • 155
  • 161
  • Will try. Is this be possible to do with the UIScrollViewDelegate methods instead of observing contentSize? – hpique Aug 22 '09 at 17:37
  • Most likely; you'd use `- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale` (described at http://developer.apple.com/iphone/library/documentation/UIKit/Reference/UIScrollViewDelegate_Protocol/Reference/UIScrollViewDelegate.html#//apple_ref/doc/uid/TP40006923-CH3-SW6), but I'd prefer observing `contentSize` because otherwise you wind up discarding the new zoom scale and finding it from the view anyway. (Unless you can pull off some awesome math based on the relative sizes of your views.) – Tim Aug 22 '09 at 22:19
  • Thanks, Tim. scrollViewDidEndZooming is what I'm using. See code in question (I've added it after your first reply). It almost works. The only problem that remains if that if I pinch the image after minimumZoomScale has been reached, it returns to the top left corner. – hpique Aug 24 '09 at 05:02
  • Do you mean that it works (centers the image) for pinching from any scale other than the minimum zoom scale, and only breaks if you try to pinch it again once you're at the minimum scale? Or does it not work for pinching to the minimum scale at all? – Tim Aug 24 '09 at 08:16
  • The first one. It centers the image for pinching from any scale other than the minimum zoom scale, and it breaks if you try to pinch it again once you're at the minimum scale. Also, when it reaches the minimum zoom scale the first time, the content is briefly animated like coming from the top, which cause a brief weird effect. This happens on the 3.0 simulator at least. – hpique Aug 24 '09 at 08:57
  • Huh. Maybe you can use a `BOOL` ivar to remember when you're already at the minimum zoom scale, and not apply your custom inset logic (or indeed do much of anything) when it receives a pinch at the minimum scale? And as for the animation effect, that could either be a Simulator quirk or an actual issue - can you try it on a device and see which it is? – Tim Aug 24 '09 at 10:15
  • Tried to do nothing if minimumZoomScale has been reached, but it breaks anyway. Whatever code makes it break it's not coming from scrollViewDidEndZooming. Will try on the device to confirm it's not a simulator quirk. – hpique Aug 24 '09 at 13:22
  • In that case, hgpc, I'm kind of stuck for ideas :) Sorry! – Tim Jan 02 '10 at 20:42
1

This is my solution to that problem which works pretty fine for any kind of view inside a scrollview.

-(void)scrollViewDidZoom:(__unused UIScrollView *)scrollView 
    {
    CGFloat top;
    CGFloat left;
    CGFloat bottom;
    CGFloat right;

    if (_scrollView.contentSize.width < scrollView.bounds.size.width) {
        DDLogInfo(@"contentSize %@",NSStringFromCGSize(_scrollView.contentSize));

        CGFloat width = (_scrollView.bounds.size.width-_scrollView.contentSize.width)/2.0;

        left = width;
        right = width;


    }else {
        left = kInset;
        right = kInset;
    }

    if (_scrollView.contentSize.height < scrollView.bounds.size.height) {

        CGFloat height = (_scrollView.bounds.size.height-_scrollView.contentSize.height)/2.0;

        top = height;
        bottom = height;

    }else {
        top = kInset;
        right = kInset;
    }

    _scrollView.contentInset = UIEdgeInsetsMake(top, left, bottom, right);



  if ([self.tiledScrollViewDelegate respondsToSelector:@selector(tiledScrollViewDidZoom:)])
  {
        [self.tiledScrollViewDelegate tiledScrollViewDidZoom:self];
  }
}
Bartek
  • 1,049
  • 6
  • 18
beat843796
  • 11
  • 2
1

There are a plenty of solutions here, but I'd risk putting here my own. It's good for two reasons: it doesn't mess zooming experience, as would do updating image view frame in progress, and also it respects original scroll view insets (say, defined in xib or storyboard for graceful handling of semi-transparent toolbars etc).

First, define a small helper:

CGSize CGSizeWithAspectFit(CGSize containerSize, CGSize contentSize) {
    CGFloat containerAspect = containerSize.width / containerSize.height,
            contentAspect = contentSize.width / contentSize.height;

    CGFloat scale = containerAspect > contentAspect
                    ? containerSize.height / contentSize.height
                    : containerSize.width / contentSize.width;

    return CGSizeMake(contentSize.width * scale, contentSize.height * scale);
}

To retain original insets, defined field:

UIEdgeInsets originalScrollViewInsets;

And somewhere in viewDidLoad fill it:

originalScrollViewInsets = self.scrollView.contentInset;

To place UIImageView into UIScrollView (assuming UIImage itself is in loadedImage var):

CGSize containerSize = self.scrollView.bounds.size;
containerSize.height -= originalScrollViewInsets.top + originalScrollViewInsets.bottom;
containerSize.width -= originalScrollViewInsets.left + originalScrollViewInsets.right;

CGSize contentSize = CGSizeWithAspectFit(containerSize, loadedImage.size);

UIImageView *imageView = [[UIImageView alloc] initWithFrame:(CGRect) { CGPointZero, contentSize }];
imageView.autoresizingMask = UIViewAutoresizingNone;
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.image = loadedImage;

[self.scrollView addSubview:imageView];
self.scrollView.contentSize = contentSize;

[self centerImageViewInScrollView];

scrollViewDidZoom: from UIScrollViewDelegate for that scroll view:

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
    if (scrollView == self.scrollView) {
        [self centerImageViewInScrollView];
    }
}

An finally, centering itself:

- (void)centerImageViewInScrollView {
    CGFloat excessiveWidth = MAX(0.0, self.scrollView.bounds.size.width - self.scrollView.contentSize.width),
            excessiveHeight = MAX(0.0, self.scrollView.bounds.size.height - self.scrollView.contentSize.height),
            insetX = excessiveWidth / 2.0,
            insetY = excessiveHeight / 2.0;

    self.scrollView.contentInset = UIEdgeInsetsMake(
            MAX(insetY, originalScrollViewInsets.top),
            MAX(insetX, originalScrollViewInsets.left),
            MAX(insetY, originalScrollViewInsets.bottom),
            MAX(insetX, originalScrollViewInsets.right)
    );
}

I didn't test orientation change yet (i.e. proper reaction for resizing UIScrollView itself), but fix for that should be relatively easy.

jazzcat
  • 534
  • 7
  • 11
1

Apple's Photo Scroller Example does exactly what you are looking for. Put this in your UIScrollView Subclass and change _zoomView to be your UIImageView.

-(void)layoutSubviews{
  [super layoutSubviews];
  // center the zoom view as it becomes smaller than the size of the screen
  CGSize boundsSize = self.bounds.size;
  CGRect frameToCenter = self.imageView.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;
  }
  self.imageView.frame = frameToCenter; 
}

Apple's Photo Scroller Sample Code

Yedy
  • 2,018
  • 1
  • 24
  • 30
Korey Hinton
  • 2,384
  • 2
  • 23
  • 24
1

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.

  • Couldn't make this to work. It seems you're centering the scrollView and not its content. Why does the window size matter? Shouldn't the code correct the x position as well? – hpique Jan 02 '10 at 16:40
1

To make the animation flow nicely, set

self.scrollview.bouncesZoom = NO;

and use this function (finding the center using the method at this answer)

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
    [UIView animateWithDuration:0.2 animations:^{
        float offsetX = MAX((scrollView.bounds.size.width-scrollView.contentSize.width)/2, 0);
        float offsetY = MAX((scrollView.bounds.size.height-scrollView.contentSize.height)/2, 0);
        self.imageCoverView.center = CGPointMake(scrollView.contentSize.width*0.5+offsetX, scrollView.contentSize.height*0.5+offsetY);
    }];
}

This creates the bouncing effect but doesn't involve any sudden movements beforehand.

Community
  • 1
  • 1
ldanilek
  • 331
  • 1
  • 8
1

In case your inner imageView has initial specific width(eg 300) and you just want to center its width only on zoom smaller than its initial width this might help you also.

 func scrollViewDidZoom(scrollView: UIScrollView){
    if imageView.frame.size.width < 300{
        imageView.center.x = self.view.frame.width/2
    }
  }
dejix
  • 1,134
  • 12
  • 22
1

You'll find that the solution posted by Erdemus does work, but… There are some cases where the scrollViewDidZoom method does not get invoked & your image is stuck to the top left corner. A simple solution is to explicitly invoke the method when you initially display an image, like this:

[self scrollViewDidZoom: scrollView];

In many cases, you may be invoking this method twice, but this is a cleaner solution than some of the other answers in this topic.

Maulik
  • 19,108
  • 14
  • 80
  • 136
russes
  • 1,052
  • 11
  • 15
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];
}
knbk
  • 46,344
  • 6
  • 103
  • 109
Jonah
  • 4,770
  • 13
  • 60
  • 75
0

Here's the current way I'm making this work. It's better but still not perfect. Try setting:

 myScrollView.bouncesZoom = YES; 

to fix the problem with the view not centering when at minZoomScale.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGSize screenSize = [[self view] bounds].size;//[[UIScreen mainScreen] bounds].size;//
CGSize photoSize = [yourImage size];
CGFloat topInset = (screenSize.height - photoSize.height * [myScrollView zoomScale]) / 2.0;
CGFloat sideInset = (screenSize.width - photoSize.width * [myScrollView zoomScale]) / 2.0;

if (topInset < 0.0)
{ topInset = 0.0; }
if (sideInset < 0.0)
{ sideInset = 0.0; } 
[myScrollView setContentInset:UIEdgeInsetsMake(topInset, sideInset, -topInset, -sideInset)];
ApplicationDelegate *appDelegate = (ApplicationDelegate *)[[UIApplication sharedApplication] delegate];

CGFloat scrollViewHeight; //Used later to calculate the height of the scrollView
if (appDelegate.navigationController.navigationBar.hidden == YES) //If the NavBar is Hidden, set scrollViewHeight to 480
{ scrollViewHeight = 480; }
if (appDelegate.navigationController.navigationBar.hidden == NO) //If the NavBar not Hidden, set scrollViewHeight to 360
{ scrollViewHeight = 368; }

imageView.frame = CGRectMake(0, 0, CGImageGetWidth(yourImage)* [myScrollView zoomScale], CGImageGetHeight(yourImage)* [myScrollView zoomScale]);

[imageView setContentMode:UIViewContentModeCenter];
}

Also, I do the following to prevent the image from sticking a the side after zooming out.

- (void) scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
myScrollView.frame = CGRectMake(0, 0, 320, 420);
 //put the correct parameters for your scroll view width and height above
}
knbk
  • 46,344
  • 6
  • 103
  • 109
Jonah
  • 4,770
  • 13
  • 60
  • 75
  • Hi Jonah. It seems we've been dealing with the same issue for a while. Will check your two solutions and reply soon. – hpique Jan 06 '10 at 01:15
  • Hi Jonah, I tried your latest solution. However, what is temporaryImage ? I tried putting temporaryImage = imageView.image ; however, once I zoom, the image disappears. Thanks, Pannag – psanketi Jan 26 '11 at 02:22
  • Pannag, temporaryImage was poorly named. It should be called myImage as it is just whatever picture you are using. Sorry for the confusion. – Jonah Jan 26 '11 at 02:22
0

Just disable the pagination, so it'll work fine:

scrollview.pagingEnabled = NO;
etolstoy
  • 1,788
  • 20
  • 33
Nagaraj
  • 792
  • 7
  • 11
0

I had the exact same problem. Here is how I solved

This code should get called as the result of scrollView:DidScroll:

CGFloat imageHeight = self.imageView.frame.size.width * self.imageView.image.size.height / self.imageView.image.size.width;
BOOL imageSmallerThanContent = (imageHeight < self.scrollview.frame.size.height) ? YES : NO;
CGFloat topOffset = (self.imageView.frame.size.height - imageHeight) / 2;

// If image is not large enough setup content offset in a way that image is centered and not vertically scrollable
if (imageSmallerThanContent) {
     topOffset = topOffset - ((self.scrollview.frame.size.height - imageHeight)/2);
}

self.scrollview.contentInset = UIEdgeInsetsMake(topOffset * -1, 0, topOffset * -1, 0);
aryaxt
  • 69,636
  • 87
  • 281
  • 421
0

Although the question is a bit old yet the problem still exists. I solved it in Xcode 7 by making the vertical space constraint of the uppermost item (in this case the topLabel) to the superViews (the scrollView) top an IBOutlet and then recalculating its constant every time the content changes depending on the height of the scrollView's subviews (topLabel and bottomLabel).

class MyViewController: UIViewController {

    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var topLabel: UILabel!
    @IBOutlet weak var bottomLabel: UILabel!
    @IBOutlet weak var toTopConstraint: NSLayoutConstraint!

    override func viewDidLayoutSubviews() {
        let heightOfScrollViewContents = (topLabel.frame.origin.y + topLabel.frame.size.height - bottomLabel.frame.origin.y)
        // In my case abs() delivers the perfect result, but you could also check if the heightOfScrollViewContents is greater than 0.
        toTopConstraint.constant = abs((scrollView.frame.height - heightOfScrollViewContents) / 2)
    }

    func refreshContents() {
        // Set the label's text …

        self.view.layoutIfNeeded()
    }
}
Nick Podratz
  • 622
  • 8
  • 17