I have a UIViewController and it's view hierarchy looks like this:
UIView
UIScrollView
UIImageView
I have code that positions the image view in the middle of the scroll view's frame, like so:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self recenterContent:scrollView];
}
- (void)recenterContent:(UIScrollView *)scrollView {
//this centers the content when it is smaller than the scrollView's bounds
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);
}
This works fine when zooming the content, but when the view controller first loads it does not center. This is because the scrollView.contentSize
is always 0
. So my question is - when should I call this method after the scrollView.contentSize is set? When does that get set?
I have tried in viewDidLayoutSubviews
, and the bounds of the scroll view is set then, but not the content size. Is there some method that I can use where the scroll view will be guaranteed to have the content size set?
Or is there a better way to keep the image centered when it is smaller than the scroll view? What I am trying to accomplish is to have it so the image view is not at the top of the scroll view and what I am using works, except when the scroll view's content size is not set. But if there is a better way of doing this without having to adjust the contentInset
, I would be fine with that too.
Here is what I have currently.
It is almost working, but no matter what I try, I cannot get it to look correct when the view loads. The way it works now is that it starts out off-center because when it calls the recenterContent
method, before the view is displayed the content size of the scroll view is CGSizeZero, so the calculations are wrong. But if I try to recenter the content after the view has been displayed, then there is a visible delay before it gets centered.
I am just confused as to when the contentSize of the scroll view is set if I am using AutoLayout constraints to specify the size.
Here is my code. Can anyone see anything wrong with it?
@interface MyImageViewController ()
@property (strong, nonatomic) UIScrollView *scrollView;
@property (strong, nonatomic) UIImageView *imageView;
@property (assign, nonatomic) BOOL needsZoomScale;
@end
@implementation MyImageViewController
- (void)loadView {
self.view = [[UIView alloc] init];
[self.view addSubview:self.scrollView];
[self.scrollView addSubview:self.imageView];
self.needsZoomScale = YES;
[NSLayoutConstraint activateConstraints:@[
[self.scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.scrollView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[self.scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[self.imageView.leadingAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.leadingAnchor],
[self.imageView.topAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.topAnchor],
[self.imageView.trailingAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.trailingAnchor],
[self.imageView.bottomAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.bottomAnchor]
]];
}
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTapZoom:)];
doubleTapGesture.numberOfTapsRequired = 2;
[self.imageView addGestureRecognizer:doubleTapGesture];
}
- (CGRect)zoomRectForScrollView:(UIScrollView *)scrollView withScale:(CGFloat)scale withCenter:(CGPoint)center {
CGRect zoomRect;
//the zoom rect is in the content view's coordinates. At a zoom scale of 1.0, the zoom rect would be the size
//of the scroll view's bounds. As the zoom scale decreases, so more content is visible, the size of the rect
//grows.
zoomRect.size.width = scrollView.frame.size.width / scale;
zoomRect.size.height = scrollView.frame.size.height / scale;
//choose an origin so as to get the right center
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);
return zoomRect;
}
- (void)doubleTapZoom:(UITapGestureRecognizer *)sender {
UIView *tappedView = sender.view;
CGPoint tappedPoint = [sender locationInView:tappedView];
if (tappedPoint.x <= 0) {
tappedPoint.x = 1;
}
if (tappedPoint.y <= 0) {
tappedPoint.y = 1;
}
if (tappedPoint.x >= tappedView.bounds.size.width) {
tappedPoint.x = tappedView.bounds.size.width - 1;
}
if (tappedPoint.y >= tappedView.bounds.size.height) {
tappedPoint.y = tappedView.bounds.size.height - 1;
}
CGFloat zoomScale;
if (self.scrollView.zoomScale < 1) {
zoomScale = 1;
} else if (self.scrollView.zoomScale < self.scrollView.maximumZoomScale) {
zoomScale = self.scrollView.maximumZoomScale;
} else {
zoomScale = self.scrollView.minimumZoomScale;
}
CGRect zoomRect = [self zoomRectForScrollView:self.scrollView withScale:zoomScale withCenter:tappedPoint];
[self.scrollView zoomToRect:zoomRect animated:YES];
}
- (UIScrollView *)scrollView {
if (!self->_scrollView) {
self->_scrollView = [[UIScrollView alloc] init];
self->_scrollView.translatesAutoresizingMaskIntoConstraints = NO;
self->_scrollView.minimumZoomScale = 0.1f;
self->_scrollView.maximumZoomScale = 4.0f;
self->_scrollView.bounces = YES;
self->_scrollView.bouncesZoom = YES;
self->_scrollView.delegate = self;
self->_scrollView.backgroundColor = [UIColor blackColor];
}
return self->_scrollView;
}
- (UIImageView *)imageView {
if (!self->_imageView) {
self->_imageView = [[UIImageView alloc] init];
self->_imageView.translatesAutoresizingMaskIntoConstraints = NO;
self->_imageView.userInteractionEnabled = YES;
}
return self->_imageView;
}
- (UIImage *)image {
return self.imageView.image;
}
- (void)setImage:(UIImage *)image {
self.imageView.image = image;
self.needsZoomScale = YES;
[self updateZoomScale];
}
- (void)updateZoomScale {
if (self.needsZoomScale && self.image) {
CGSize size = self.view.bounds.size;
if (size.width == 0.0f || size.height == 0.0f) {
return;
}
UIImage *image = self.image;
CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
if (imageSize.width > 0 && imageSize.height > 0) {
CGFloat widthScale = size.width / imageSize.width;
CGFloat heightScale = size.height / imageSize.height;
CGFloat minScale = MIN(widthScale, heightScale);
self.scrollView.minimumZoomScale = minScale;
self.scrollView.zoomScale = minScale;
self.needsZoomScale = NO;
}
}
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self updateZoomScale];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self recenterContent:self.scrollView];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self recenterContent:self.scrollView];
}
#pragma mark - UIScrollViewDelegate
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self recenterContent:scrollView];
}
- (void)recenterContent:(UIScrollView *)scrollView {
//this centers the content when it is smaller than the scrollView's bounds
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);
}
@end
The problem is that a UIImageView
has an intrinsic content size of 0,0
-- so your code is initially putting the a 0x0
image view at the center of the scroll view.
I've made a few changes to the code you posted... see comments (I "wrapped" the changes in
// ---------------------------------
comment lines:
@interface MyImageViewController : UIViewController <UIScrollViewDelegate>
@end
@interface MyImageViewController ()
@property (strong, nonatomic) UIScrollView *scrollView;
@property (strong, nonatomic) UIImageView *imageView;
@property (assign, nonatomic) BOOL needsZoomScale;
@end
@implementation MyImageViewController
- (void)loadView {
self.view = [[UIView alloc] init];
[self.view addSubview:self.scrollView];
[self.scrollView addSubview:self.imageView];
self.needsZoomScale = YES;
// ---------------------------------
// respect safe area
UILayoutGuide *g = [self.view safeAreaLayoutGuide];
// saves on a little typing
UILayoutGuide *sg = [self.scrollView contentLayoutGuide];
// ---------------------------------
[NSLayoutConstraint activateConstraints:@[
[self.scrollView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor],
[self.scrollView.topAnchor constraintEqualToAnchor:g.topAnchor],
[self.scrollView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor],
[self.scrollView.bottomAnchor constraintEqualToAnchor:g.bottomAnchor],
[self.imageView.leadingAnchor constraintEqualToAnchor:sg.leadingAnchor],
[self.imageView.topAnchor constraintEqualToAnchor:sg.topAnchor],
[self.imageView.trailingAnchor constraintEqualToAnchor:sg.trailingAnchor],
[self.imageView.bottomAnchor constraintEqualToAnchor:sg.bottomAnchor]
]];
}
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTapZoom:)];
doubleTapGesture.numberOfTapsRequired = 2;
[self.imageView addGestureRecognizer:doubleTapGesture];
}
- (CGRect)zoomRectForScrollView:(UIScrollView *)scrollView withScale:(CGFloat)scale withCenter:(CGPoint)center {
CGRect zoomRect;
//the zoom rect is in the content view's coordinates. At a zoom scale of 1.0, the zoom rect would be the size
//of the scroll view's bounds. As the zoom scale decreases, so more content is visible, the size of the rect
//grows.
zoomRect.size.width = scrollView.frame.size.width / scale;
zoomRect.size.height = scrollView.frame.size.height / scale;
//choose an origin so as to get the right center
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);
return zoomRect;
}
- (void)doubleTapZoom:(UITapGestureRecognizer *)sender {
UIView *tappedView = sender.view;
CGPoint tappedPoint = [sender locationInView:tappedView];
if (tappedPoint.x <= 0) {
tappedPoint.x = 1;
}
if (tappedPoint.y <= 0) {
tappedPoint.y = 1;
}
if (tappedPoint.x >= tappedView.bounds.size.width) {
tappedPoint.x = tappedView.bounds.size.width - 1;
}
if (tappedPoint.y >= tappedView.bounds.size.height) {
tappedPoint.y = tappedView.bounds.size.height - 1;
}
CGFloat zoomScale;
if (self.scrollView.zoomScale < 1) {
zoomScale = 1;
} else if (self.scrollView.zoomScale < self.scrollView.maximumZoomScale) {
zoomScale = self.scrollView.maximumZoomScale;
} else {
zoomScale = self.scrollView.minimumZoomScale;
}
CGRect zoomRect = [self zoomRectForScrollView:self.scrollView withScale:zoomScale withCenter:tappedPoint];
[self.scrollView zoomToRect:zoomRect animated:YES];
}
- (UIScrollView *)scrollView {
if (!self->_scrollView) {
self->_scrollView = [[UIScrollView alloc] init];
self->_scrollView.translatesAutoresizingMaskIntoConstraints = NO;
self->_scrollView.minimumZoomScale = 0.1f;
self->_scrollView.maximumZoomScale = 4.0f;
self->_scrollView.bounces = YES;
self->_scrollView.bouncesZoom = YES;
self->_scrollView.delegate = self;
self->_scrollView.backgroundColor = [UIColor blackColor];
}
return self->_scrollView;
}
- (UIImageView *)imageView {
if (!self->_imageView) {
self->_imageView = [[UIImageView alloc] init];
self->_imageView.translatesAutoresizingMaskIntoConstraints = NO;
self->_imageView.userInteractionEnabled = YES;
}
return self->_imageView;
}
- (UIImage *)image {
return self.imageView.image;
}
- (void)setImage:(UIImage *)image {
self.imageView.image = image;
// ---------------------------------
// set the frame here
self.imageView.frame = CGRectMake(0.0, 0.0, image.size.width, image.size.height);
// ---------------------------------
// not needed ... unless maybe changing the image while view is showing?
//self.needsZoomScale = YES;
//[self updateZoomScale];
}
- (void)updateZoomScale {
if (self.needsZoomScale && self.image) {
CGSize size = self.view.bounds.size;
if (size.width == 0.0f || size.height == 0.0f) {
return;
}
UIImage *image = self.image;
CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
if (imageSize.width > 0 && imageSize.height > 0) {
CGFloat widthScale = size.width / imageSize.width;
CGFloat heightScale = size.height / imageSize.height;
CGFloat minScale = MIN(widthScale, heightScale);
self.scrollView.minimumZoomScale = minScale;
self.scrollView.zoomScale = minScale;
self.needsZoomScale = NO;
}
}
}
// ---------------------------------
// Don't need this
//- (void)viewWillLayoutSubviews {
// [super viewWillLayoutSubviews];
// [self updateZoomScale];
//}
// ---------------------------------
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// ---------------------------------
// update zoom scale here
[self updateZoomScale];
// ---------------------------------
[self recenterContent:self.scrollView];
}
// ---------------------------------
// Don't need this
//- (void)viewDidAppear:(BOOL)animated {
// [super viewDidAppear:animated];
// [self recenterContent:self.scrollView];
//}
// ---------------------------------
#pragma mark - UIScrollViewDelegate
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self recenterContent:scrollView];
}
- (void)recenterContent:(UIScrollView *)scrollView {
//this centers the content when it is smaller than the scrollView's bounds
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);
}
@end
and here's how I call it:
MyImageViewController *vc = [MyImageViewController new];
UIImage *img = [UIImage imageNamed:@"bkg"];
if (nil == img) {
NSLog(@"Could not load image!!!!");
return;
}
[vc setImage:img];
[self.navigationController pushViewController:vc animated:YES];