UIScrollView CRUD

Korey Hinton Blog

UIScrollView

UIScrollView is a great technology that has led to many other controls that have built on top of it, such as UITableView, UICollectionView, and UITextView. If any of the higher level controls can easily solve your problem go with those, but sometimes you need something more custom and UIScrollView is great at allowing you to create scrollable content.

If you want to play along, the full source code to this example is found in the CRUD section below.

../media/scrollbar.png

Create

Should I use auto layout?

It depends. Usually, with UIScrollView I use auto layout for static content that does not get created or changed on the fly. Usually when you are displaying scrollable content, you have content you want to be created dynamically. Dynamic content works great programmatically since you won't be constrained to auto layout constraints and frame-based content size calculation will generally be less complex than trying to re-arrange auto layout constraints. If you know what the content will always be and look like then auto layout constraints work great without having to do a bunch of frame math.

Create UIScrollView programmatically

Make UIScrollView a property of your view controller so you can reference it in multiple places.

var scrollView : UIScrollView!

Your view controller's viewDidLoad method is a great place to initialize a UIScrollView object. Notice in the code below we are not setting the frame of the scrollview yet because the bounds of the view controller's view are not correctly set yet at this point. In viewDidLoad:

scrollView = UIScrollView()
scrollView.delegate = self
scrollView.contentSize = CGSizeMake(2000, 2000)
view.addSubview(scrollView)

Use a container view

Its best practice to put all elements that will go into the scrollview into a containing view. So the container will be a subview of UIScrollView and all other elements will be nested underneath the container view. This is a good design decision because if we were to implement zooming we are required to give a single view as the zooming view. To get all elements to zoom we'd need a container.

var containerView : UIView!

Now viewDidLoad looks like this:

override func viewDidLoad() {
    super.viewDidLoad()
    scrollView = UIScrollView()
    scrollView.delegate = self
    scrollView.contentSize = CGSizeMake(2000, 2000)

    containerView = UIView()

    scrollView.addSubview(containerView)
    view.addSubview(scrollView)    
}

Set its frame

Since the bounds of the view controller's view is not ready in viewDidLoad, I like to do frame setting in viewDidLayoutSubviews which is guaranteed to not break on rotation. Since we are basing the containerView's frame on the scrollView's contentSize we could have moved the last line of code into viewDidLoad but anything that will be calculated based off the view's bounds directly or indirectly must not be put in viewDidLoad.

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    scrollView.frame = view.bounds
    containerView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height)
}

Other attributes

I usually set attributes on the scrollView in viewDidLoad. Since we will be zooming we set the minimum and maximum zoom scale factors. We will not be turning off bouncing in the CRUD example later in this article. I just wanted to show how to do it. While the bouncing feel does look cool when you scroll to the beginning or end of the scrollview it also doesn't fit with every context. I'm currently working on a simple grid game using UIScrollView and I found that bouncing takes away from the game-feel. Yes, animations are cool in games but sometimes the default built-in animations don't feel game-like. So I set the bounces property to false. Note that it defaults to true so you don't need to set that property if you do want it to bounce.

scrollView.minimumZoomScale = 0.5
scrollView.maximumZoomScale = 2.0
scrollView.bounces = false

Read

With UIScrollView the read part is the hardest. To be able to see what is happening to the scrollview we need to conform to the UIScrollViewDelegate protocol, set the view controller as the delegate, and implement the scrollview delegate callback methods. Notice there are quite a few and you must take all the relevant ones into consideration or your going to see some strange bugs. The way I like to approach this is encapsulating the read code into a single method that introspects the UIScrollView object and updates as necessary based on its state. Each relevant callback will call this method. Here are some useful properties to check for:

scrollView.zooming
scrollView.dragging
scrollView.contentOffset.x
scrollView.contentOffset.y

Notice I am using these properties in the read function of the CRUD example later in this tutorial to figure out whether it is stopped, panning, zooming, or both.

Update

Update sounds like a weird term for a user interface control. Update makes perfect sense for a table in a database. So what are some of the things we can do to update or change the scrollView? For one we can update the zoomScale property to zoom in or out. Also, we can update the contentOffset property to scroll to a particular location. Another thing we can do to change the scrollView is to add a subview to our container.

containerView.addSubview(myView)

Sometimes I like to combine the zoom and offset updates into a single method:

func update(zoomScale: CGFloat, offSet: CGPoint) {
    scrollView.zoomScale = zoomScale
    scrollView.contentOffset = offSet
}

Delete

Deleting from a scrollView would mean simply removing elements from the container. If you wanted to no longer show the scrollView itself you could do scrollView.removeFromSuperView() or set its hidden property to true. In the example below we will add views by tapping on the screen and then delete views by tapping on those views. We use the view.tag property to easily later identify these views with containerView.viewWithTag(myView.tag)

CRUD

The following example is meant to be copied and pasted over the ViewController.swift file in a new Single View Application. Just create a new Single View project in Swift and replace the entire contents of ViewController with the following:

import UIKit

class ViewController : UIViewController, UIScrollViewDelegate, UITextFieldDelegate {

    var scrollView : UIScrollView!

    /*
     * As a subview to the Scroll View it will be repositioned as the user pans
     * Also is the view used for zooming
     */
    var containerView : UIView!

    var plusButton : UIButton!
    var minusButton : UIButton!
    var statusLabel : UILabel!
    var xField : UITextField!
    var yField : UITextField!

    var viewCount = 1

    // UIViewController

    override func viewDidLoad() {
        super.viewDidLoad()
        create()
        read()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        scrollView.frame = view.bounds
        containerView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height)
        minusButton.frame = CGRectMake(0, 20, 50, 50)
        plusButton.frame = CGRectMake(50, 20, 50, 50)
        statusLabel.frame = CGRectMake(20, 70, 300, 80)
        xField.frame = CGRectMake(20, 150, 100, 80)
        yField.frame = CGRectMake(120, 150, 100, 80)
    }

    /******************************
     * CRUD
     * Create, Read, Update, Delete
     *****************************/

    // Create

    func create() {

        scrollView = UIScrollView()
        scrollView.delegate = self
        scrollView.minimumZoomScale = 0.5
        scrollView.maximumZoomScale = 2.0
        scrollView.contentSize = CGSizeMake(2000, 2000)

        containerView = UIView()

        containerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "viewTapped:"))

        buildControls()

        scrollView.addSubview(containerView)

        view.addSubview(scrollView)

    }

    // Read

    func read() {
        var text = ""
        if scrollView.dragging {
            text = "Panning scroll... "
        }
        if scrollView.zooming {
            text += "Pinch zoom... "
        }
        if (!scrollView.zooming && !scrollView.dragging)
        {
            text += "Idle"
        }
        statusLabel.text = text
        xField.text = "\(scrollView.contentOffset.x)"
        yField.text = "\(scrollView.contentOffset.y)"
    }

    // Update

    func update(zoomScale: CGFloat, offSet: CGPoint) {
        scrollView.zoomScale = zoomScale
        scrollView.contentOffset = offSet
    }

    func addView(view: UIView, tag: Int) {
        view.tag = tag
        containerView.addSubview(view)
    }

    // Delete

    func deleteView(tag: Int) {
        containerView.viewWithTag(tag)?.removeFromSuperview()
    }


    // Controls

    func plus(sender : UIButton) {
        update(scrollView.zoomScale+0.1, offSet: CGPointZero)
    }
    func minus(sender : UIButton) {
        update(scrollView.zoomScale-0.1, offSet: CGPointZero)
    }

    func buildControls() {

        minusButton = UIButton.buttonWithType(.System) as UIButton
        minusButton.setTitle("-", forState: .Normal)
        minusButton.addTarget(self, action: "minus:", forControlEvents: .TouchUpInside)

        plusButton = UIButton.buttonWithType(.System) as UIButton
        plusButton.setTitle("+", forState: .Normal)
        plusButton.addTarget(self, action: "plus:", forControlEvents: .TouchUpInside)

        xField = UITextField()
        xField.placeholder = "x"
        xField.delegate = self
        yField = UITextField()
        yField.placeholder = "y"
        yField.delegate = self

        statusLabel = UILabel()

        containerView.addSubview(plusButton)
        containerView.addSubview(minusButton)
        containerView.addSubview(statusLabel)
        containerView.addSubview(xField)
        containerView.addSubview(yField)
    }

    func viewTapped(gesture : UITapGestureRecognizer) {
        if gesture.view == containerView {
            var v = UIView(frame: CGRectMake(0, 0, 100, 100))
            v.center = gesture.locationInView(containerView)
            v.backgroundColor = UIColor.redColor()
            v.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "viewTapped:"))
            addView(v, tag: viewCount)
            viewCount = viewCount + 1
        } else {
            deleteView(gesture.view!.tag)
        }
    }


    // UIScrollViewDelegate

    func scrollViewWillBeginZooming(scrollView: UIScrollView, withView view: UIView!) {
        read()
    }
    func scrollViewDidZoom(scrollView: UIScrollView) {
        read()
    }
    func scrollViewDidEndZooming(scrollView: UIScrollView, withView view: UIView!, atScale scale: CGFloat) {
        read()
    }
    func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
        read()
    }
    func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        read()
    }
    func scrollViewWillBeginDragging(scrollView: UIScrollView) {
        read()
    }
    func scrollViewDidScroll(scrollView: UIScrollView) {
        read()
    }
    func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
        return containerView
    }

    // UITextFieldDelegate

    func textFieldShouldReturn(textField: UITextField) -> Bool {
        let offset = CGPointMake(CGFloat((xField.text as NSString).floatValue), (CGFloat((yField.text as NSString).floatValue)))
        update(scrollView.zoomScale, offSet: offset)
        return true
    }
}

More CRUD

I like learning UIKit controls by figuring out the Create, Read, Update, Delete functionalities. So stay tuned for more articles like this one. Also, check out my UIToolbar CRUD article. Thanks!

Date: 2014-12-27T15:36+0000

Author: Korey Hinton

Org version 7.9.3f with Emacs version 24

Validate XHTML 1.0