ashipma
ashipma

Reputation: 475

UIScrollView - when is contentSize set

I have a UIViewController and it's view hierarchy looks like this:

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.


Update

Here is what I have currently.

enter image description here

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

Upvotes: 0

Views: 644

Answers (1)

DonMag
DonMag

Reputation: 77690

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

Upvotes: 1

Related Questions