32
votes

I'm trying to implement a UIScrollView the New Way, using Auto Layout. I've set up constraints from the inner view to the scroll view so that it can compute its own contentSize automatically, and that works like a charm— except that all hell breaks loose when I try to zoom in or out. I can't even properly describe what happens, other than to say that the inner view gets "messed up".

You can see an example of this behavior here (not my project; you have to set the scroll view's maximumZoomScale and implement -viewForZoomingInScrollView: before zooming will work).

Has anyone else run into this behavior? Is there currently any way to get zooming in a UIScrollView to work with Auto Layout without essentially re-implementing the zooming behavior yourself?

3
I know what you mean. I'm having the same problem. Did you figure it out? It seems to have something to do with the constraints, and I've tried removing the constraints in scrollViewWillBeginZooming:withView: and putting them back on in scrollViewDidEndZooming:withView:atScale: and during the zoom it works better, but after the zoom the content is in the wrong place.devguydavid
@david: No, I never solved this. My conclusion was that various parts of UIKit simply are not compatible with Auto Layout yet, and as a result I've stopped using Auto Layout in those cases (UIScrollView and UIPageViewController in particular).phu
I too have this issue. I have a scroll view with embedded imageView and am trying to just allow simple zooming of the image and it is all funky and distorts the entire imageview while jumping around sizes while pinching the view. Wish someone would find a fix for this.JimmyJammed

3 Answers

4
votes

The best answer that I have seen is Mark's (https://stackoverflow.com/users/1051919/mark-kryzhanouski), posted here: UIScrollView Zoom Does Not Work With Autolayout.

The crux of it is that you have to anchor the image view that is nested in the scroll view, to the parent of the scroll view. Despite the guidance in the iOS 6 release notes, it is not intuitive to me what view is "floating" over what. In this case, the scrolling view is just a single image view.

I did do a lot of experimentation with this, hoping to find an all-IB approach and found none. You can still generate the view hierarchy in IB, but you still have to programatically add constraints. You can delete some or all of the default constraints (mainly just to appease the constraint-conflict warnings), but you always need Mark's code to tie the image view to the parent of the scroll view, the grand-parent of the image view.

It seems like it should be simpler than this - it "should just work" but:

NSDictionary *viewsDictionary = @{ @"scrollView": self.scrollView, @"imageView": self.imageView };
[self.view addConstraints:[NSLayoutConstraint
    constraintsWithVisualFormat:@"H:|[imageView(width)]"
    options:0
    metrics:@{@"width": @(self.imageView.image.size.width)}
    views:viewsDictionary]];

[self.view addConstraints:[NSLayoutConstraint
    constraintsWithVisualFormat:@"V:|[imageView(height)]"
    options:0
    metrics:@{@"height": @(self.imageView.image.size.height)}
    views:viewsDictionary]];
3
votes

Without adding an imageView in the storyboard, I've found the following works perfectly:

-(UIImageView *)imageView
{
    if (!_imageView) _imageView = [[UIImageView alloc] initWithFrame:CGRectZero];
    return _imageView;
}
- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.scrollView addSubview:self.imageView];
    // Set the min and max:
    self.scrollView.minimumZoomScale = 0.2;
    self.scrollView.maximumZoomScale = 5.0;
    self.scrollView.delegate = self;

    // Set the content:
    self.scrollView.zoomScale = 1.0; // reset zoomScale for new image
    self.scrollView.contentSize = CGSizeMake(image.size.width/2, image.size.height/2);
    self.imageView.frame = CGRectMake(0, 0, image.size.width/2, image.size.height/2);
    self.imageView.image = image;
}

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

Complete Swift Playground Example

The simplest example I can think of is adding a UIImageView to a UIScrollView. This is 100% in code, you'll just need to add a PNG to the Playground. I called mine Image.png. In a Playground you'll see the whole thing rendered in the 'Live View'. Pinch-zoom works using a Ctrl-click to place one finger on the screen and then dragging around. Until the content is zoomed in bigger than the screen panning will not work. Double-tap the image to toggle between 1x and 3x scale.

Based upon the Apple's Technical Note TN2154: UIScrollView and Autolayout

Gotcha

You'll find the whole thing very frustrating if your content is not bigger than the screen size. If your content completely fits on screen nothing will happen. That's why you must get zooming to work too. If you want to prove to yourself it works, test with a really big image (bigger than the window).

import UIKit
import PlaygroundSupport

enum TapToggle {
    case Regular, Large
}

class ScrollingViewController : UIViewController
{
    var tapToggle: TapToggle = .Large
    var scrollView: UIScrollView?
    var imageView: UIImageView?

    override func viewDidLoad()
    {
        let image = UIImage(named: "Image")
        let imageView = UIImageView(image: image)
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.backgroundColor = .white
        imageView.isUserInteractionEnabled = true

        let scrollView = UIScrollView()
        scrollView.minimumZoomScale = 0.5
        scrollView.maximumZoomScale = 10.0
        scrollView.delegate = self
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(imageView)
        let imageViewKey = "imageView"
        let imageViews = [imageViewKey: imageView]
        scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[\(imageViewKey)]|", options: [], metrics: nil, views: imageViews))
        scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[\(imageViewKey)]|", options: [], metrics: nil, views: imageViews))
        self.imageView = imageView

        scrollView.backgroundColor = .white
        self.view.addSubview(scrollView)

        let scrollViewKey = "scrollView"
        let scrollViews = [scrollViewKey: scrollView]
        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[\(scrollViewKey)]|", options: [], metrics: nil, views: scrollViews))
        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[\(scrollViewKey)]|", options: [], metrics: nil, views: scrollViews))

        self.scrollView = scrollView

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTap(sender:)))
        tapGesture.numberOfTapsRequired = 2
        self.imageView?.addGestureRecognizer(tapGesture)
    }

    @objc
    public func didDoubleTap(sender: AnyObject)
    {
        switch self.tapToggle {
        case .Regular:
            self.scrollView?.zoomScale = 1.0
            self.tapToggle = .Large
        case .Large:
            self.scrollView?.zoomScale = 3.0
            self.tapToggle = .Regular
        }
    }
}

extension ScrollingViewController: UIScrollViewDelegate
{
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return self.imageView
    }

    func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat)
    {
        print("\(scale)")
    }
}

PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = ScrollingViewController()