Swift Swift: Using UIScrollView with Autolayout

Last week we talked about cameras, one thing I’d like in my camera app is the ability to take a picture then zoom and scroll around the photo. For that, we need to use the UIScrollview. UIScrollView is a very flexible and important control in UIKit. It is the basis for many the controls we all know and love including UItableView. IT’s what does all the pinch zoom and pan on many applications like maps. I love UItableView and UIPicker, but more than once I’d like that does the same thing moving sideways. besides zooming I’ll cover that too. Most importantly, in iOS8 we can’t get around auto layout, which messes up just about everything that traditional approaches to UIScrollView does. I’ll show you a good way of handling things.

Set up the views

we are going to use two UIScrollViews for this in auto layout. Make a new project named SwiftPizzaScrollview with a single view in Swift and universal. Drag two UIScrollViews on the screen and anchor one to the bottom and one to the top. Make 10 point vertical space between them, and equal heights. Update the frames then change the equal height to a 1:4 proportion. IF that made no sense to you, follow the video below:

Once you have the layout completed, open the aassistant editor and set to automatic so you see the viewController.Swift class. Control drag from the top UIScrollView to the class and make an outlet named buttonScroll. then control drag from the bottom UIScrollView and make a outlet named imageScroll.

    @IBOutlet weak var imageScroll: UIScrollView!
    @IBOutlet weak var buttonScroll: UIScrollView!
  

Setting up the Button Scroll

The first scroll we will look at is the button scroll. Scroll views work by having view added to them and the contentSize property being bigger than the size of the scroll view. We’ll need a big view for this. For this one, I’ll make a programmatic row of buttons that select a color. Under the outlets, add this code to make a function to create the buttons:

    func colorButtonsView(buttonSize:CGSize, buttonCount:Int) -> UIView {
    //creates color buttons in a UIView
        let buttonView = UIView()

        return buttonView()
}

This makes a view, but doesn’t do much, let’s start by defining a few properties for the view. We’ll make the background black, and set the origin to 0,0.
Add this after the buttonView declaration:

        buttonView.backgroundColor = UIColor.blackColor()
        buttonView.frame.origin = CGPointMake(0,0)

Next we need to size the view. However to size the view we have to calculate its size based on the size of the buttons and the padding added to it.

let padding = CGSizeMake(10, 10)
buttonView.frame.size.width = (buttonSize.width + padding.width) * CGFloat(buttonCount)
buttonView.frame.size.height = (buttonSize.height +  2.0 * padding.height)

Line 1 I made a constant for the padding I can use in my calculations. The width of the view in line 2 would be the size of the button and one side of padding multiplied by the number of buttons. The height in line 3 is the height of the button with a top and bottom padding.

Our strategy to add the buttons is to loop through ans add buttons each loop. Along the way we will change the x origin of the new button, and change the color using a HSB hue in UIColor(hue:saturation:brightness:alpha) We’ll need some initial values before we start the loop:

//add buttons to the view
        var buttonPosition = CGPointMake(padding.width * 0.5, padding.height)
        let buttonIncrement = buttonSize.width + padding.width
        let hueIncrement = 1.0 / CGFloat(buttonCount)
        var newHue = hueIncrement

Line two starts us at a place with some padding. Line 3 is the distance between buttons on the x axis. line 4 is the distance between colors we will show. Since colors are represented between 1.0 and 0 this will give the number of colors. We initialize a value for the hue we will use.

Next we start the loop. Add the loop like this:

for i in 0...(buttonCount - 1)  {
            var button = UIButton.buttonWithType(.Custom) as UIButton
            button.frame.size = buttonSize
            button.frame.origin = buttonPosition
            buttonPosition.x = buttonPosition.x + buttonIncrement
            button.backgroundColor = UIColor(hue: newHue, saturation: 1.0, brightness: 1.0, alpha: 1.0)
            newHue = newHue + hueIncrement
            button.addTarget(self, action: "colorButtonPressed:", forControlEvents: .TouchUpInside)
            buttonView.addSubview(button)
        }

We make a button, set it size and origin, then increment the origin for the next loop. Similarly, we set the background color of the hue to a color, then increment the hue to the next color. We add an action to the button and the add the button to the view. when the loop finished we have a complete view we can return and do what we need with it.

Only thing missing is the target. Add this under the last method:

func colorButtonPressed(sender:UIButton){
        buttonScroll.backgroundColor = sender.backgroundColor
 }

Which just take the background color and makes the buttonScroll that color.

Making the sliding button scroll view

To make the scroll view run, we need the following code in the viewDidLoad:

      //scrolling pageview
        let scrollingView = colorButtonsView(CGSizeMake(100.0,50.0), buttonCount: 10)
        buttonScroll.contentSize = scrollingView.frame.size
        buttonScroll.addSubview(scrollingView)
        buttonScroll.showsHorizontalScrollIndicator = true
        buttonScroll.indicatorStyle = .Default

Line 1 gets a view. once we have the view we have it’s size which we make the contentSize of the scrollingView. If you skip this step the scroll view will not work. We add the view to the buttonScroll and we are set tot go. The last two lines add a scroll indicator for cosmetic purposes.
Build and run. You get a color picker that side scrolls.

Screenshot 2014-12-10 16.14.13

Using a Xib

We used for this example a programmatically generated view. It’s not the only way to go. While I didn’t use this, you could use a xib if you wanted various controls in the code. Set up a view controller with a xib. Set an absolute height and width for the xib’s view to make it easy to get a contentView size. Then declare the view controller (again this isn’t part of our app)

let scrollingViewController = UIViewController(nibName: "buttonScrollViewController", bundle: nil)

Instead of line 1 above in the viewDidLoad use this:

let scrollingView = scrollingViewController.view

This will allow you to put any control in the scroll view, and use the xib’s view controller to control it.

Scrolling a Picture

We’ll need a photo for this. We’ll use this one of a pizza:

pizza

Download the image and click on Images.Xassets in Xcode. Drag the image into the assets from your finder.

Just like the scrolling buttons we just have to make a view, add it to the scroll view and set the content size. Next make an UIImageView in the declaration just below the outlets:

 var imageView = UIImageView(image: UIImage(named: "pizza"))

And then we add this to the viewDidLoad

imageScroll.contentSize = imageView.frame.size
imageScroll.addSubview(imageView)

Build and run. The photo is 640×640 so depending on the simulator you pick you will get different scrolling behaviors. Use a small device like the iPhone 4s and you will get more scrolling.

Zooming in a scroll view with Auto Layout

If you look across the web, you will find a lot of articles on zooming with UIScrollView. If you use Auto Layout, you will quickly and frustratingly learn auto layout has problem with the way most prope write for UIScrollView. Some turn off auto layout to get around this. The problem is a moving target. A Zoom needs several things:

  1. Add the UIScrollViewDelegate
  2. Set the delegate
  3. Implement the delegate method viewForZoomingInScrollView
  4. Set the maximumZoomScale property
  5. Set the minimumZoomScale property
  6. Set the current zoom

The maximumZoomScale and minimumZoomScale properties of the UIScrollView cannot be equal on order for zoom to work. It’s these scales which messes everything up. Most people set their zoom by making a minimum zoom as a ratio of the bounds of the scroll view and the content view. This usually get coded in viewDidLoad, except auto layout does not set the bounds of the scroll view that early. It’s one of the last things to happen. So all of that code is looking for something either that is the wrong size, or doesn’t exist yet.

The way around this is overriding didLayoutSubViews. This happens after any changes to the bounds, and all the bounds are actually set. This takes care of both auto layout not knowing anything on the initial load, and any time we change orientation. Both cases are caught. There is one caution to this. The content size will change in the process of a rotation. We will need to reset it every time we go change the scale. To make this easy, I made a reference property for the class

 var imageSize = CGSizeMake(0,0)

You could keep calling the imageView’s frame if you wanted, but it seems verbose. Instead in viewDidLoad I assign it and the delegate, and set the content size in didLayoutSubviews. Change what we had for the imageScroll code in viewDidLoad to this:

//zooming photo
        imageSize = imageView.frame.size
        imageScroll.delegate = self
        imageScroll.addSubview(imageView)
        imageScroll.showsHorizontalScrollIndicator = false
        imageScroll.showsVerticalScrollIndicator = false

Much of this code we discussed already. I added the scroll indicator properties explicitly instead of its defaults.
now add the following just below viewDidLoad:

  override func viewDidLayoutSubviews() {
            imageScroll.maximumZoomScale = 5.0
            imageScroll.contentSize = imageSize
            let widthScale = imageScroll.bounds.size.width / imageSize.width
            let heightScale = imageScroll.bounds.size.height / imageSize.height
            imageScroll.minimumZoomScale = min(widthScale, heightScale)
            imageScroll.setZoomScale(max(widthScale, heightScale), animated: true )
    }

Line 2 sets the maximum zoom scale. I used an arbitrary value of five times the size of the image. I then set the content size. As we’ve already said, the scroll view needs this, but we are keeping the size under control by using a constant, since the content view changes when we rotate the view, and can mess up our calculations. Next we figure the scale by taking the bounds of the scroll View, which by now are correct since auto layout is done with them. We see if the widthScale or heightScale is smaller, and use that scale as the one for our minimum zoom scale. We then set the scale for the scroll view as the minimum zoom scale. You can do a lot with this code and modify it a lot of different ways for different scale effects. This is the basics.

Our last step is to set up the delegate, which is a quick one. It just wants the view we are zooming to return to the delegate

//MARK: Delegates
    func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
        return imageView
    }

Build and run. You get a working scroll view.

Screenshot 2014-12-10 16.34.05
You can do a lot from this point. The scrolling anchor point for example is the origin at (0,0) some people like zooming from a center point. The big thing you need to do is get your sizing information after auto layout does what it does.

26 Replies to “Swift Swift: Using UIScrollView with Autolayout”

  1. Hi there I would love to see detailed post on how to transition between different tableviews. I’m confused about how to code to segue from on tableview to another in an app. e.g. if screen 1 was dog cat snake and you wanted to go to screen 2 labrador,husky poodle but cat would show lion tiger etc how you would code for that.
    Or indeed if you could create a collection view which would then segue to a tableview?

    1. That is a great question!!!!

      I’ll try to make up a example of that for a January Swift Swift. in the meantime I’ll give you my thinking for it in the simplest form.

      First you’ll need an object for the animals, so make this class or something like it:

      class Animal{
      var name:String = "Animal Name"
      var subspecies = ["Animal 1","Animal 2","Animal 3"] 
      }
      

      For your first tableview, you would have an array of type Animal, and use the index from the array in tableView:didSelectRowAtIndexPath: to access the animal’s name to display on the table view. When an animal gets selected, your segue or however you are getting to your second controller would take the selected index, and send the selected animal object to your second view controller as a property myAnimal:Animal. In your second view controller you would use the myAnimal.subspecies[index] property to get your names in tableView:didSelectRowAtIndexPath: where index = indexPath.row to show the subspecies of animals. That would be the simplest way.

      There are few other ways too if you used SQL or coreData, but that gets complicated.

      Hope that helps

      (edited for clarity a few times)

      1. Hi thanks for that! although I’m still a little lost to be honest :(
        I created a github repository of the project I’ve created so far https://github.com/edlen/Animal-App-tableview.git but it doesn’t have the code for going between the screens I was wondering would you mind taking a look at it and maybe letting me know how I could proceed? If there was anyway you could even suggest how to segue from dog to labrador to the info screen on labrador I’d really appreciate it!

      2. No problem it was kinda cheeky of me to ask! I’ll keep an eye out for the example in jan- looking forward to the book!

  2. Hi
    Great tutorial, but how do u simulate zooming the image? Have tried option/click and I can see the zoom control but image does not appear to be zooming..

      1. Would it possible to change the horizontal scroll to contain a series image views, then update the main vertical scroll zoomed image view when the a imag view is touched in the horizontal view?

        Not sure where we might update the zoomed image view as the image view is set in view didload and scroll in viewDidLayoutSubviews…..

      2. If I am understanding what you want. You might want to read a bit about table views and collection views. these will do the job much easier than coding all of that yourself. Collection views is available in the newsletter downloads site right now, but will be released publicly soon. Sign up for the newsletter and you will get directions on how to get there.

      3. Thanks will do, had a quick look but I am not certain if u can pinch/zoom in out of images using a collection view?

  3. Hey, thank you for your tutorial on UIScrollViews, but trying your code my program crashes at that point I press one of these buttons. It tells me “libc++abi.dylib: terminating with uncaught exception of type NSException” and “Thread 1: signal SIGABRT”.

    I dunno how to handle this.. Where’s the problem? I exactly used your code for the Button Scroll…
    Thanks in advance :)

      1. Thought it might be something like that. You get that in a storyboard and programmatically when the button and target are wired up to an unknown identifier.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s