Keep zoomable image in center of UIScrollView

Awesome!

Thanks for the code :)

Just thought I'd add to this as I changed it slightly to improve the behaviour.

// make the change during scrollViewDidScroll instead of didEndScrolling...
-(void)scrollViewDidZoom:(UIScrollView *)scrollView
{
    CGSize imgViewSize = self.imageView.frame.size;
    CGSize imageSize = self.imageView.image.size;

    CGSize realImgSize;
    if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
        realImgSize = CGSizeMake(imgViewSize.width, imgViewSize.width / imageSize.width * imageSize.height);
    }
    else {
        realImgSize = CGSizeMake(imgViewSize.height / imageSize.height * imageSize.width, imgViewSize.height);
    }

    CGRect fr = CGRectMake(0, 0, 0, 0);
    fr.size = realImgSize;
    self.imageView.frame = fr;

    CGSize scrSize = scrollView.frame.size;
    float offx = (scrSize.width > realImgSize.width ? (scrSize.width - realImgSize.width) / 2 : 0);
    float offy = (scrSize.height > realImgSize.height ? (scrSize.height - realImgSize.height) / 2 : 0);

    // don't animate the change.
    scrollView.contentInset = UIEdgeInsetsMake(offy, offx, offy, offx);
}

Here's my solution that works universally with any tab bar or navigation bar combination or w/o both, translucent or not.

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
  // The scroll view has zoomed, so you need to re-center the contents
  CGSize scrollViewSize = [self scrollViewVisibleSize];
  // First assume that image center coincides with the contents box center.
  // This is correct when the image is bigger than scrollView due to zoom
  CGPoint imageCenter = CGPointMake(self.scrollView.contentSize.width/2.0,
                                    self.scrollView.contentSize.height/2.0);

  CGPoint scrollViewCenter = [self scrollViewCenter];

  //if image is smaller than the scrollView visible size - fix the image center accordingly
  if (self.scrollView.contentSize.width < scrollViewSize.width) {
    imageCenter.x = scrollViewCenter.x;
  }

  if (self.scrollView.contentSize.height < scrollViewSize.height) {
    imageCenter.y = scrollViewCenter.y;
  }

  self.imageView.center = imageCenter;
}


//return the scroll view center
- (CGPoint)scrollViewCenter {
  CGSize scrollViewSize = [self scrollViewVisibleSize];
  return CGPointMake(scrollViewSize.width/2.0, scrollViewSize.height/2.0);
}


// Return scrollview size without the area overlapping with tab and nav bar.
- (CGSize) scrollViewVisibleSize {
  UIEdgeInsets contentInset = self.scrollView.contentInset;
  CGSize scrollViewSize = CGRectStandardize(self.scrollView.bounds).size;
  CGFloat width = scrollViewSize.width - contentInset.left - contentInset.right;
  CGFloat height = scrollViewSize.height - contentInset.top - contentInset.bottom;
  return CGSizeMake(width, height);
}

Swift 5:

public func scrollViewDidZoom(_ scrollView: UIScrollView) {
    centerScrollViewContents()
}

private var scrollViewVisibleSize: CGSize {
    let contentInset = scrollView.contentInset
    let scrollViewSize = scrollView.bounds.standardized.size
    let width = scrollViewSize.width - contentInset.left - contentInset.right
    let height = scrollViewSize.height - contentInset.top - contentInset.bottom
    return CGSize(width:width, height:height)
}

private var scrollViewCenter: CGPoint {
    let scrollViewSize = self.scrollViewVisibleSize()
    return CGPoint(x: scrollViewSize.width / 2.0,
                   y: scrollViewSize.height / 2.0)
}

private func centerScrollViewContents() {
    guard let image = imageView.image else {
        return
    }

    let imgViewSize = imageView.frame.size
    let imageSize = image.size

    var realImgSize: CGSize
    if imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height {
        realImgSize = CGSize(width: imgViewSize.width,height: imgViewSize.width / imageSize.width * imageSize.height)
    } else {
        realImgSize = CGSize(width: imgViewSize.height / imageSize.height * imageSize.width, height: imgViewSize.height)
    }

    var frame = CGRect.zero
    frame.size = realImgSize
    imageView.frame = frame

    let screenSize  = scrollView.frame.size
    let offx = screenSize.width > realImgSize.width ? (screenSize.width - realImgSize.width) / 2 : 0
    let offy = screenSize.height > realImgSize.height ? (screenSize.height - realImgSize.height) / 2 : 0
    scrollView.contentInset = UIEdgeInsets(top: offy,
                                           left: offx,
                                           bottom: offy,
                                           right: offx)

    // The scroll view has zoomed, so you need to re-center the contents
    let scrollViewSize = scrollViewVisibleSize

    // First assume that image center coincides with the contents box center.
    // This is correct when the image is bigger than scrollView due to zoom
    var imageCenter = CGPoint(x: scrollView.contentSize.width / 2.0,
                              y: scrollView.contentSize.height / 2.0)

    let center = scrollViewCenter

    //if image is smaller than the scrollView visible size - fix the image center accordingly
    if scrollView.contentSize.width < scrollViewSize.width {
        imageCenter.x = center.x
    }

    if scrollView.contentSize.height < scrollViewSize.height {
        imageCenter.y = center.y
    }

    imageView.center = imageCenter
}

Why it's better than anything else I could find on SO so far:

  1. It doesn't read or modify the UIView frame property of the image view since a zoomed image view has a transform applied to it. See here what Apple says on how to move or adjust a view size when a non identity transform is applied.

  2. Starting iOS 7 where translucency for bars was introduced the system will auto adjust the scroll view size, scroll content insets and scroll indicators offsets. Thus you should not modify these in your code as well.

FYI: There're check boxes for toggling this behavior (which is set by default) in the Xcode interface builder. You can find it in the view controller attributes:

automatic scroll view adjustments

The full view controller's source code is published here.

Also you can download the whole Xcode project to see the scroll view constraints setup and play around with 3 different presets in the storyboard by moving the initial controller pointer to any the following paths:

  1. View with both translucent tab and nav bars.
  2. View with both opaque tab and nav bars.
  3. View with no bars at all.

Every option works correctly with the same VC implementation.


I think I got it. The solution is to use the scrollViewDidEndZooming method of the delegate and in that method set contentInset based on the size of the image. Here's what the method looks like:

- (void)scrollViewDidEndZooming:(UIScrollView *)aScrollView withView:(UIView *)view atScale:(float)scale {
    CGSize imgViewSize = imageView.frame.size;
    CGSize imageSize = imageView.image.size;

    CGSize realImgSize;
    if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
        realImgSize = CGSizeMake(imgViewSize.width, imgViewSize.width / imageSize.width * imageSize.height);
    }
    else {
        realImgSize = CGSizeMake(imgViewSize.height / imageSize.height * imageSize.width, imgViewSize.height);
    }

    CGRect fr = CGRectMake(0, 0, 0, 0);
    fr.size = realImgSize;
    imageView.frame = fr;

    CGSize scrSize = scrollView.frame.size;
    float offx = (scrSize.width > realImgSize.width ? (scrSize.width - realImgSize.width) / 2 : 0);
    float offy = (scrSize.height > realImgSize.height ? (scrSize.height - realImgSize.height) / 2 : 0);
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:0.25];
    scrollView.contentInset = UIEdgeInsetsMake(offy, offx, offy, offx);
    [UIView commitAnimations];
}

Note that I'm using animation on setting the inset, otherwise the image jumps inside the scrollview when the insets are added. With animation it slides to the center. I'm using UIView beginAnimation and commitAnimation instead of animation block, because I need to have the app run on iphone 3.