Written by Alex Gibson, on March 30, 2017


 

Zoomable UIScrollView

Ever wondered how to easily create a view that allows a user to zoom in on an image or text.  I am sure you have as this is a common expectation from users.  We will walk through how to implement this in less than 5 minutes and approximately 5 lines of code.

First, let’s create a new Xcode project as a Single View Application.  Name it anything you want.  I am naming mine ZoomScrollView.

Open up the storyboard and immediately drag a UIScrollView onto the storyboard.

Pin it to all sides of the view as seen in the image with the pin menu. If you want the scrollview to go under the status bar I recommend you drag it to the top beyond the top layout guide before you add the constraints.

 

The second step is to drag an empty UIView into the scrollview.

Pin it to all sides with the pin menu.  But wait! Why do we have red warnings with autolayout?

Auto Layout does not always seem to be so auto but in this case it is our fault.  Why? Well a UIView has no intrinsic size to tell our scrollview how big it is which means the scrollview is mad at us for not knowing the content size.  Thankfully, this is an easy fix.  Time to use two of my favorite constraints.  Drag from our UIView inside the scrollview to the main view of the viewcontroller and choose equal heights. See image.

Repeat the step above and choose equal widths.  We have now given our scrollview enough information to size itself and all is happy in the world of xcode.  Your constraints for this view should now look like this.

 

Now let’s add a UIImageView and start by pinning to all 4 sides using our pin menu.  We will edit this in a minute.

Notice Xcode is made at us again.  I recommend to fix the autolayout warning you drag an image into your asset folder.  I grabbed a flower off the website pexels.  This is a great site for beautiful images.  Now set your image in storyboard to the test image you just added to your project. My imageview now looks like this.

Well auto layout is happy again but our image looks pretty bad.  It is stretched because of the content mode of the UIImageView. We could fix this by simply changing the content mode of the UIImageView but we are actually going to change this with constraints.

The other thing you may be wondering is why is autolayout happy at this point.  Well remember earlier when i mentioned intrinsic content size.  A UIImageView has an intrinsic content size when it actually has an image set.  It uses the image to set the size of the UIImageView.   So in your project I would recommend having a placeholder image set to always keep it happy.

To the next issue.  How to fix the image from being stretched without changing the content mode.  We are going to edit the top and bottom constraints to be >= to 0.  Click on the Top Contraint in the inspector.

 

Change the = sign to a >= and repeat this for the bottom constraint.

 

Xcode is made at us again but this is another easy fix and we will be finished with the storyboard.  Drag from our UIImageView to the view that is holding it in the UIScrollView.

 

When the menu appears choose center vertically and all is happy again.   Your constraints on the UIImageView should now look like this and your storyboard should look similar depending on the size of the image you used as your test image.

Now to the code and I promised only a few lines of code right.  

To start coding we need to connect our storyboard views to our ViewController.

Drag from the UIScrollView to the ViewController.

Name the view scrollView.  Repeat this for the view that is holding our UIImageView and name it containerView.

Your controller should now look like this.


import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var containerView: UIView!
    @IBOutlet weak var scrollView: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

Now to the code.  Make the ViewController adopt UIScrollViewDelegate and in the ViewController in viewDidLoad we are going to set the max zoom scale for our scrollview, the minimum zoom scale for the scrollview, and the ViewController to be the delegate of the scrollview.  It will look like this.


import UIKit

class ViewController: UIViewController,UIScrollViewDelegate {

    @IBOutlet weak var containerView: UIView!
    @IBOutlet weak var scrollView: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.maximumZoomScale = 5.0
        scrollView.minimumZoomScale = 1.0
        scrollView.delegate = self
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

 

Now if your run the project and try to do a pinch gesture nothing will happen at this point.  We need to return a view to zoom.  Which view will it be.  In this case we are going to zoom the containerView.  Implement viewForZooming.


import UIKit

class ViewController: UIViewController,UIScrollViewDelegate {

    @IBOutlet weak var containerView: UIView!
    @IBOutlet weak var scrollView: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.maximumZoomScale = 5.0
        scrollView.minimumZoomScale = 1.0
        scrollView.delegate = self
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return containerView
    }

}

Now run the project.  We can zoom. Yay!

There is only 1 other problem that I am not happy with.  When the scrollview is scrolled out past the point the image is at the normal size the image seems to go to the top left of the screen.  I promised to keep the code short so there is a quick fix and a longer fix.  I will show you the quick fix first.  Go to storyboard and select the scrollview and uncheck Bounces Zoom and rerun the project.

 

This mostly fixes everything and if you are happy you can stop here.  If however you want to leave this on to give a more natural bouncy feel we need to add a bit more code so add the check mark back in storyboard and hop back to ViewController file.  We are going to adjust the scrollview insets on scroll. Other tutorials out there will have you messing with constraints.  Personally the less I have to deal with constraints the happier I am so we are going to do it with the properties of the scrollview. Now you have to remember how we set up the constraints.  The image will always be pinned to the leading and trailing and will only vary based on the height.  So on the pinch to zoom the only content offset changed on our scrollview will be vertical or contentOffset.y.  This helps us know how to write the next function to deal with this zoom out issue.  So create a function called adjustScrollViewInsets().  If the content offset is < 0 that means the user has scaled the view past the natural point zooming out of the image so we need to center the zoomed view using the insets of the scrollview. If the scrollview content offset is >= zero the insets need to be zero because that works fine right now.  Putting these two sentences into code the function will look like this.


func adjustScrollViewInsets(){
        if scrollView.contentOffset.y < 0{
            let leftMargin = (scrollView.frame.size.width - self.containerView.frame.size.width) * 0.5
            let topMargin = (scrollView.frame.size.height - self.containerView.frame.size.height) * 0.5
            scrollView.contentInset = UIEdgeInsets(top: topMargin, left: leftMargin, bottom: 0, right: 0)
        }else{
            if scrollView.contentInset != .zero{
                scrollView.contentInset = .zero
            }
        }
    }

You can see that this will adjust the insets of the scrollview only if the content offset is < 0 and if the the content offset is > 0 if the scrollview does not already have a contentInset of 0 then we make sure it is 0. Why the the check if it is zero in the else statement.  Well to keep it smooth I think setting a property that is already the right value makes no sense so this is a way to optimize it.  Now we need to add the scrollview delegate that will call this function.  That method is scrollViewDidScroll().  Call the adjustScrollViewInsets() from there and the final controller will look like this.


import UIKit

class ViewController: UIViewController,UIScrollViewDelegate {

    @IBOutlet weak var containerView: UIView!
    @IBOutlet weak var scrollView: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.maximumZoomScale = 5.0
        scrollView.minimumZoomScale = 1.0
        scrollView.delegate = self
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return containerView
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        adjustScrollViewInsets()
    }
    
    func adjustScrollViewInsets(){
        if scrollView.contentOffset.y < 0{
            let leftMargin = (scrollView.frame.size.width - self.containerView.frame.size.width) * 0.5
            let topMargin = (scrollView.frame.size.height - self.containerView.frame.size.height) * 0.5
            scrollView.contentInset = UIEdgeInsets(top: topMargin, left: leftMargin, bottom: 0, right: 0)
        }else{
            if scrollView.contentInset != .zero{
                scrollView.contentInset = .zero
            }
        }
    }
}

Now run the project and see that you have a bouncy zoomable UIScrollView.  Was that as hard as you thought it would be?   I hope you enjoyed this and learned about autolayout and scrollviews.  Let me know what you think.  The final project is on Github.