38

Zooming with UIScrollView using a strictly autolayout environment does not seem to work.

This is especially frustrating because the iOS 6 release notes certainly lead me to believe it should when the wrote about a "Pure Auto Layout approach" here http://developer.apple.com/library/ios/#releasenotes/General/RN-iOSSDK-6_0/_index.html

I looked the the WWDC 2012 slides for sessions 202, 228, and 232 and didn't see an answer for this.

The only question I've seen on the internet specifically for this issue is UIScrollView zooming with Auto Layout, but it doesn't provide code of the problem and there is no answer.

This user https://stackoverflow.com/users/341994/matt has given many great responses to UIScrollView autolayout questions and even linked to code on git hub, but I haven't been able to find anything that answers this issue there.

I have attempted to boil this issue down to the absolute minimum to make it clear.

I created a new single view application with a storyboard, and made no changes in the interface builder.

I added a large picture file to the project "pic.jpg".

SVFViewController.h

#import <UIKit/UIKit.h> 
@interface SVFViewController : UIViewController <UIScrollViewDelegate>
@property (nonatomic) UIImageView *imageViewPointer;
@end

SVFViewController.m

#import "SVFViewController.h"

@interface SVFViewController ()

@end

@implementation SVFViewController


- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    UIScrollView *scrollView = [[UIScrollView alloc] init];
    UIImageView *imageView = [[UIImageView alloc] init];
    [imageView setImage:[UIImage imageNamed:@"pic.jpg"]];
    [self.view addSubview:scrollView];
    [scrollView addSubview:imageView];

    scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    imageView.translatesAutoresizingMaskIntoConstraints = NO;
    self.imageViewPointer = imageView;
    scrollView.maximumZoomScale = 2;
    scrollView.minimumZoomScale = .5;
    scrollView.delegate = self;

    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(scrollView,imageView);
    NSLog(@"Current views dictionary: %@", viewsDictionary);
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
    [scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[imageView]|" options:0 metrics: 0 views:viewsDictionary]];
    [scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView]|" options:0 metrics: 0 views:viewsDictionary]];
}

-(UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView{
    return self.imageViewPointer;
}

@end

Notice I made a particular effort to make this as much like the sample code provided in the iOS 6 release notes, just doing the bare minimum to implement zooming.

So, the problem?

When you run this application and pan around in the scroll view, everything is good. But when you zoom the problem is obvious, the image flickers back and forth, and the placement of the image within the scroll view gets more wrong with every zoom.

It looks like there is battle going on for the content offset of the imageView, it seems it is being set to different values by two different things with every "zoom". (an NSLog of the content offset property of the imageView appears to confirm this).

What am I doing wrong here? Does anyone know how to property implement zooming within a UIScrollView in an purely autolayout environment. Is there an example of this anywhere out there?

Please help.

Community
  • 1
  • 1
Corey
  • 590
  • 1
  • 5
  • 15
  • I came across this (http://developer.apple.com/library/ios/#samplecode/PhotoScroller/Introduction/Intro.html), which concerns me more than answers my question. Yes, it implements UIScrollView zoom using autolayout, but it does so by subclassing UIScrollView in a complicated way that I can't follow. There's got to be an easier way to do this... – Corey Jan 23 '13 at 07:12
  • 1
    I'm having the same problem. Embedding UIImageView inside a UIScrollView is not as straightforward as it used to be now with autolayout. – Diego Allen Jan 24 '13 at 18:28

6 Answers6

14

Once again, re-reading the iOS SDK 6.0 release notes I found that:

Note that you can make a subview of the scroll view appear to float (not scroll) over the other scrolling content by creating constraints between the view and a view outside the scroll view’s subtree, such as the scroll view’s superview.

Solution

Connect your subview to the outer view. In another words, to the view in which scrollview is embedded.

And applying constraints in following way I've got it work:

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[imageView(width)]" options:0 metrics:@{@"width":@(self.imageViewPointer.image.size.width)} views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView(height)]" options:0 metrics:@{@"height":@(self.imageViewPointer.image.size.height)} views:viewsDictionary]];
Mark Kryzhanouski
  • 7,251
  • 2
  • 22
  • 22
  • I tried implementing this solution, but upon zooming I get a "Unable to simultaneously satisfy constraints" message. I wish I could debug further, but unfortunately I don't understand your approach. – Corey Mar 21 '13 at 07:51
  • Thanks for feedback. I've updated answer. Indeed, one constraint ('|') was redundant. – Mark Kryzhanouski Mar 21 '13 at 08:23
  • Yeah, it's not logical and hard to understand but it works. Looks like it is sort of Apple bug. Hope they will change this UIScrollView behavior in future. – Mark Kryzhanouski Mar 21 '13 at 08:33
  • 2
    Here is the simple project that works, I hope, as you expect. https://dl.dropbox.com/u/52101940/Test.zip – Mark Kryzhanouski Mar 21 '13 at 12:21
  • Thanks Mark! The project you linked does indeed work! I haven't figured out exactly why that project works and the one documented in the question above does not - yet. I guess it has something to do with the metrics bits as you pointed out. I'll keep looking at it. – Corey Mar 23 '13 at 04:03
  • Completely unexpected behavior, but it works. Wish I'd seen this earlier, as I spent so much time trying to get it to work. Only question is, can it work while paging between image views? – Jon Shier Jul 07 '14 at 21:50
  • @MarkKryzhanouski Can you please share the project again as Dropbox link is dead? Is there a chance to have it in Swift instead? – christostsang May 28 '19 at 08:01
  • Hi @christostsang The issue is not reproducible any more. I've restored example if you still need it. https://www.dropbox.com/s/8g7c55puc840jpc/Test.zip?dl=0 – Mark Kryzhanouski May 28 '19 at 08:51
  • @MarkKryzhanouski For only zoom effect this works like charm.. but I am facing issue for imageview with zoom plus rotation, if I rotate image with gesture and then try to zoom, sometimes it goes out of scrollview's space hence disappears.. can you please help me in this? – Van Apr 08 '20 at 12:42
10

The issues that occurs is the changing of location of the imageview during the zoom process. The origin location of the imageview will change to be a negative value during the zoom. I believe this is why the jerky movement occurs. As well, after the zoom is complete the imageView is still in the wrong location meaning that scrolls will appear to be offset.

If you implement -(void) scrollViewDidZoom:(UIScrollView *)scrollView and log the frame of the UIImageView during this time you can see its origin changing.

I ended up making things work out by implementing a strategy like this

And in addition changing the frame of the contentView while zooming

-(void) scrollViewDidZoom:(UIScrollView *)scrollView {
    CGRect cFrame = self.contentView.frame;
    cFrame.origin = CGPointZero;
    self.contentView.frame = cFrame;
}
Ram kiran Pachigolla
  • 20,897
  • 15
  • 57
  • 78
Brett
  • 769
  • 6
  • 16
  • I appreciate your comment! Unfortunately, I still wasn't able to get my simple photo example documented above to work based on the strategy you linked. – Corey Mar 21 '13 at 07:38
  • 3
    FYI - github link is broken – powers Oct 23 '14 at 20:13
4

These solutions all kinda work. Here is what I did, no hacks or subclasses required, with this setup:

[view]
  [scrollView]
    [container]
      [imageView]
      [imageView2]
  1. In IB, hook up top, leading, bottom and trailing of scrollView to view.
  2. Hook up top, leading, bottom and trailing of container to scrollView.
  3. Hook up center-x and center-y of container to center-x and center-y of scrollView and mark it as remove on build time. This is only needed to silence the warnings in IB.
  4. Implement viewForZoomingInScrollView: in the view controller, which should be scrollView's delegate, and from that method return container.
  5. When setting the imageView's image, determine minimum zoom scale (as right now it will be displayed at the native size) and set it:

    CGSize mySize = self.view.bounds.size;
    CGSize imgSize = _imageView.image.size;
    CGFloat scale = fminf(mySize.width / imgSize.width,
                          mySize.height / imgSize.height);
    _scrollView.minimumZoomScale = scale;
    _scrollView.zoomScale = scale;
    _scrollView.maximumZoomScale = 4 * scale;
    

This works for me, upon setting the image zooms the scroll view to show the entire image and allows to zoom in to 4x the initial size.

Pascal
  • 16,846
  • 4
  • 60
  • 69
  • Hm... I like your approach. But tell me, why do you have 2 imageViews inside the container? Plus don't you have to hook them up to something? What are the constraints for them? – Mihai Fratu Jun 15 '14 at 17:59
  • That's just an example, you can add anything to `container` (since that's the view being zoomed). The constraints on those image views are however you need them so that all sides are connected. – Pascal Jun 16 '14 at 09:59
  • Yeah, well I can't make it work on iOS7 or iOS8. Something is going weird and the image is not centred in the UIScrollView. Plus when zooming in and out things go completely wrong... Does it work for you? – Mihai Fratu Jun 16 '14 at 10:53
  • I had to keep the center constraints, in order to make it work (iOS 11). – d4Rk Nov 22 '17 at 12:35
  • Beautiful! Thanks, I thought pinning the center would be redundant with constraints already anchored on the four edges, but in the context of zooming it cleans up the behavior perfectly! – Laser Jul 24 '20 at 23:44
1

Let say you have in storyboard "UIImageView" inside "UIScrollView" inside "UIView".

Link all constraints in "UIScrollView" with the view controller + the two constraints in UIView (Horizontal Space - Scroll View - View & Horizontal Space - Scroll View - View).

set the view controller AS delegate for the "UIScrollView".

Then implement this code:

@interface VC () <UIScrollViewDelegate>
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray* constraints;
@end

@implementation FamilyTreeImageVC

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.scrollView removeConstraints:self.constraints];
}


- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    return self.imageView;
}

-(void) scrollViewDidZoom:(UIScrollView *)scrollView {
    CGRect cFrame = self.imageView.frame;
    cFrame.origin = CGPointZero;
    self.imageView.frame = cFrame;
}
Hizabr
  • 423
  • 5
  • 6
0

I had the same problem when trying to implement zoom from a storyboarded project using only scrollView.

I fixed it by adding a separate pinch gesture recogniser. I just dragged it from the toolbox onto my scene. Then I connected it to an action I called "doPinch" that implements the zoom. I connected it to an outlet I called "pinchRecognizer" so that I could access its scale property. This seems to override the built in zoom of the scrollView and the jumpiness disappears. Maybe it does not make the same mistake with origins, or handles that more gracefully. It is very little work on top of the layout in IB.

As soon as you introduce the pinch gesture recogniser to the scene you do need both the action and viewForZoomingInScrollView methods. Miss out either and the zooming stops working.

The code in my view controller is this:

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.zoomableImage;
}

- (IBAction)doPinch:(id)sender

{
NSLog(@"In the pinch action now with scale: %f", self.pinchRecognizer.scale);
[scrollView setZoomScale:self.pinchRecognizer.scale animated:NO];
}

This very basic implementation does have a side effect: when you come back to a zoomed image and zoom some more the value of scale is 1.0f so it jumps back to the original scale.

You can sort this out by introducing a property "currentScale" to track the scale and set the pinch gesture recogniser scale when you start zooming again. You need to use the state property of the gesture recogniser:

- (IBAction)doPinch:(id)sender
{
NSLog(@"In the pinch action now with scale: %f", self.pinchRecognizer.scale);
NSLog(@"Gesture recognizer state is: %d", self.pinchRecognizer.state);

switch (self.pinchRecognizer.state)
{
    case 1:
        NSLog(@"Zoom begins, with current scale set to: %f", self.currentScale);
        [self.pinchRecognizer setScale:self.currentScale];
        break;
    case 3:
        NSLog(@"Zoom ends, with pinch recognizer scale set to: %f", self.pinchRecognizer.scale);
        self.currentScale = self.pinchRecognizer.scale;
    default:
        break;
}

[scrollView setZoomScale:self.pinchRecognizer.scale animated:NO];
}
Tim
  • 1,108
  • 13
  • 25
  • What exactly is the pinch attached to? The ScrollView? The ImageView? The overall View? – Matt Mc Jun 27 '13 at 03:24
  • Sorry for the delay in answering - failed to spot the inbox! The pinch gesture recognizer is added to the scene in the storyboard. It seems to sit at the top level and detects pinches on any visible view in that scene. – Tim Sep 24 '13 at 15:50
0

So this is what I managed to work out.

Here's the original with my changes:

@interface ScrollViewZoomTestViewController () <UIScrollViewDelegate>

@property (nonatomic, strong) UIImageView* imageViewPointer;

// These properties are new
@property (nonatomic, strong) NSMutableArray* imageViewConstraints;
@property (nonatomic) BOOL imageViewConstraintsNeedUpdating;
@property (nonatomic, strong) UIScrollView* scrollViewPointer;

@end

@implementation ScrollViewZoomTestViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIScrollView *scrollView = [[UIScrollView alloc] init];
    UIImageView *imageView = [[UIImageView alloc] init];
    [imageView setImage:[UIImage imageNamed:@"pic.jpg"]];
    [self.view addSubview:scrollView];
    [scrollView addSubview:imageView];

    scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    imageView.translatesAutoresizingMaskIntoConstraints = NO;
    self.imageViewPointer = imageView;
    // New
    self.scrollViewPointer = scrollView;
    scrollView.maximumZoomScale = 2;
    scrollView.minimumZoomScale = .5;
    scrollView.delegate = self;

    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(scrollView, imageView);
    NSLog(@"Current views dictionary: %@", viewsDictionary);
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];

    // Saving the image view width & height constraints
    self.imageViewConstraints = [[NSMutableArray alloc] init];
    // Constrain the image view to be the same width & height of the scroll view
    [_imageViewConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[imageView(scrollView)]|" options:0 metrics: 0 views:viewsDictionary]];
    [_imageViewConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView(scrollView)]|" options:0 metrics: 0 views:viewsDictionary]];

    // Add the image view constraints to the VIEW, not the scroll view
    [self.view addConstraints:_imageViewConstraints];

    // Flag
    self.imageViewConstraintsNeedUpdating = YES;
}

So to recap here, I'm adding all of the constraints to self.view, saving the constraints set on the UIImageView in a NSMutableArray property, and setting a flag that the UIImageView constraints need updating.

These initial constraints on UIImageView work to set it up to start with. It will be the same width & height as the UIScrollView. However, this WON'T allow us to zoom the image view. It will keep it the same width / height as the scroll view. Not what we want. That's why I'm saving the constraints and setting the flag. We'll take care of that in a minute.

Now, set the view for zooming:

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    return self.imageViewPointer;
}

Ok, so now we need to actually allow us to zoom. I'm removing the initial UIImageView constraints and adding some new ones, this time constraining to the UIScrollView's contentSize width & height:

- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
{
    if(_imageViewConstraintsNeedUpdating)
    {
        // Remove the previous image view constraints
        [self.view removeConstraints:_imageViewConstraints];

        // Replace them with new ones, this time constraining against the `width` & `height` of the scroll view's content, not the scroll view itself
        NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_scrollViewPointer, _imageViewPointer);
        [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_imageViewPointer(width)]|" options:0 metrics:@{@"width" : @(_scrollViewPointer.contentSize.width)} views:viewsDictionary]];
        [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_imageViewPointer(height)]|" options:0 metrics:@{@"height" : @(_scrollViewPointer.contentSize.height)} views:viewsDictionary]];

        self.imageViewConstraintsNeedUpdating = NO;
    }
}

@end

We can't set the constraints up like this in -viewDidLoad because the image hasn't been rendered into the UIImageView yet, so UIScrollView's contentSize will be {0,0}.

This seems pretty hacky to me, but it does work, it does use pure Auto Layout, and I can't find a better way to do it. Seems to me like Apple needs to provide a better way to zoom content in a UIScrollView AND use Auto Layout constraints.

Rishil Patel
  • 1,977
  • 3
  • 14
  • 30
Jordan
  • 4,133
  • 1
  • 27
  • 43