I'm having issues with UIScrollView when doing pinch-to-zoom UIImageView. Implementation is relatively simple which you can find in many places on the Internet. Double tap to zoom in and out works perfectly fine. But pinch to zoom rarely works as expected. The main problem is that sometimes (like in 50% of cases) UIImageView is scrolled to the top although content offset is set to have padding. Easy to reproduce on horizontal images when slightly zoomed in by pinching the image. Can be reproduced on iOS 16 / 17. Appreciate any help.
#import "ViewController.h"
#import <Photos/Photos.h>
#import <PhotosUI/PhotosUI.h>
@interface ViewController () <UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIScrollViewDelegate> {
UIImageView *_imageView;
UIScrollView *_scrollView;
}
@property (nonatomic, readonly) BOOL isZoomed;
@end
@implementation ViewController
- (BOOL) isZoomed {
return _scrollView.zoomScale > _scrollView.minimumZoomScale;
}
- (void) viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
_scrollView.frame = self.view.bounds;
}
- (void) viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_scrollView = [[UIScrollView alloc] initWithFrame: self.view.bounds];
_scrollView.backgroundColor = UIColor.clearColor;
_scrollView.showsVerticalScrollIndicator = NO;
_scrollView.showsHorizontalScrollIndicator = NO;
_scrollView.delegate = self;
_scrollView.minimumZoomScale = 1.0;
_scrollView.maximumZoomScale = 5.0;
_scrollView.clipsToBounds = YES;
_scrollView.scrollsToTop = NO;
_scrollView.contentInset = UIEdgeInsetsZero;
_scrollView.translatesAutoresizingMaskIntoConstraints = NO;
_scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
[_scrollView setAutoresizesSubviews: NO];
_imageView = [[UIImageView alloc] initWithFrame: self.view.bounds];
_imageView.backgroundColor = UIColor.clearColor;
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
_imageView.contentMode = UIViewContentModeScaleAspectFit;
_imageView.autoresizingMask = UIViewAutoresizingNone;
[_imageView setUserInteractionEnabled: YES];
[_scrollView addSubview: _imageView];
[self.view addSubview: _scrollView];
[self.view sendSubviewToBack: _scrollView];
UITapGestureRecognizer *_zoomGesture = [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(onEditorDoubleTap:)];
_zoomGesture.numberOfTapsRequired = 2;
[_scrollView addGestureRecognizer: _zoomGesture];
}
- (void) resetZoom {
if (!self.isZoomed) {
return;
}
[_scrollView setZoomScale: _scrollView.minimumZoomScale animated: YES];
}
- (void) onEditorDoubleTap: (UITapGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
return;
}
if (self.isZoomed) {
[self resetZoom];
} else {
CGPoint location = [gestureRecognizer locationInView: gestureRecognizer.view];
CGPoint pointInView = [gestureRecognizer.view convertPoint: location toView: _imageView];
CGFloat newZoomScale = (_scrollView.zoomScale > _scrollView.minimumZoomScale) ? _scrollView.minimumZoomScale : _scrollView.maximumZoomScale;
CGFloat width = _scrollView.frame.size.width / newZoomScale;
CGFloat height = _scrollView.frame.size.height / newZoomScale;
CGFloat x = pointInView.x - (width / 2.0);
CGFloat y = pointInView.y - (height / 2.0);
CGRect rectToZoomTo = CGRectMake(x, y, width, height);
[_scrollView zoomToRect: rectToZoomTo animated: YES];
}
}
- (IBAction) openPhoto {
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.delegate = self;
picker.allowsEditing = NO;
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentViewController:picker animated:YES completion:nil];
}
- (void)imagePickerController: (UIImagePickerController *) picker didFinishPickingMediaWithInfo: (NSDictionary *) info {
UIImage *image = info[UIImagePickerControllerOriginalImage];
_imageView.image = image;
_imageView.transform = CGAffineTransformIdentity;
_imageView.frame = CGRectMake(0, 0, image.size.width, image.size.height);
_scrollView.frame = self.view.bounds;
_scrollView.contentSize = image.size;
CGSize scrollViewSize = _scrollView.frame.size;
CGFloat widthScale = scrollViewSize.width / image.size.width;
CGFloat heightScale = scrollViewSize.height / image.size.height;
_scrollView.minimumZoomScale = MIN(widthScale, heightScale);
_scrollView.maximumZoomScale = _scrollView.minimumZoomScale * 5.0;
_scrollView.zoomScale = _scrollView.minimumZoomScale;
[self.view bringSubviewToFront: _scrollView];
__weak __typeof__(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[weakSelf centerImageAnimated: YES];
});
[picker dismissViewControllerAnimated: YES completion: nil];
}
- (void) centerImageAnimated: (BOOL) animated {
CGSize scrollViewSize = _scrollView.frame.size;
CGFloat imageWidth = _scrollView.contentSize.width;
CGFloat imageHeigth = _scrollView.contentSize.height;
if (imageWidth > scrollViewSize.width && imageHeigth > scrollViewSize.height) {
return;
}
CGFloat horizontalPadding = (scrollViewSize.width - imageWidth) * 0.5f;
CGFloat verticalPadding = (scrollViewSize.height - imageHeigth) * 0.5f;
[_scrollView setContentOffset: CGPointMake(-horizontalPadding, -verticalPadding) animated: animated];
}
#pragma mark - UIScrollViewDelegate
- (UIView *) viewForZoomingInScrollView: (UIScrollView *) scrollView {
return _imageView;
}
- (void) scrollViewDidZoom: (UIScrollView *) scrollView {
[self centerImageAnimated: NO];
}
@end
A
UIScrollViewwill allow you to drag past the edges...So, for example, if the content is taller than the frame, we can drag-down and see a "gap" between the top of the content and the top of the scroll view frame. At that point, the
contentOffset.ybecomes negative. As soon as we stop dragging, the scroll view resetscontentOffset.yto Zero.It will also allow you to explicitly set
contentOffset.yto a negative value -- which is what your code is doing to center the content.However, as soon as
UIKitmakes a layout pass, the scroll view resetscontentOffset.yto Zero.What we need to do is modify the scroll view's
.contentInset...For example, if we need 100-points of space at the top (and bottom) to vertically center the content, we set the
.contentInset.topto 100. The.contentOffset.ywill then be Zero but we'll have the needed vertical space.Here is your
ViewControllerwith a few edits... while this has nothing to do with the layout logic, you'll notice I edited the code so I could add an image without needing to invoke the Image Picker: