Center content of UIScrollView when smaller

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)];
}

Solution 1:

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);
}

Solution 2:

@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)
}

Solution 3:

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