Bezier Paths and Gesture Recognizers

In this tutorial we’ll be creating an app that lets you play around with shapes, moving, scaling and rotating them. We’ll be UIGestureRecognizers and UIBezierPaths. The end result will look like this:

 

Detecting Taps

To get started, create a new Single View Application. Make sure to select Swift as the programming language andUniversal in the Devices tab. The app works best on an iPad because of the larger screen space.

We’ll want to add shapes to our view when the user taps the screen. We’ll be using an UITapGestureRecognizer for this. A gesture recognizer is an object that detects when a certain gesture is performed. UITapGestureRecognizer in particular detects when a tap occurs. Each gesture recognizer has one or more target-action pairs associated with it, whenever a gesture is recognized a method is called for each of the gesture recognizer’s targets. A gesture recognizer must also be attached to a view, the gesture recognizers will only receive events that happen within that view or its subviews.

Let’s add a UITapGestureRecognizer to our view controller’s view. The viewDidLoad method is a good place to add gesture recognizers to a ViewController’s view. The action we provide to the gesture recognizer has to correspond to a method in the target’s class. The method’s name is passed a string with a colon(:) at the end, the colon signifies that the method takes 1 parameter, which in this case will be the gesture recognizer.

class ViewController {
...
    func viewDidLoad() {
        ...
        let tapGR = UITapGestureRecognizer(target: self, action: "didTap:")
        self.view.addGestureRecognizer(tapGR)
    }

    func didTap(tapGR: UITapGestureRecognizer) {

    }
...
}

Adding Shapes

All shapes in our app will be instances of our ShapeView class, this class will handle the drawing and the gestures. Initially we’ll be drawing a red square but we’ll get into more complicated shapes soon!

To start off, create a new class ShapeView that subclasses UIView.

In the didTap method we can retrieve the tap’s location via the gesture recognizer’s locationInView: method. Once we have a tap position we’ll want to create a new shape at that position. Let’s create an init(origin: CGPoint) method on our ShapeView that will create the view at a given position. We’ll also define a size constant initialized to 150. In our init method we call the init(frame) method on the superclass and reposition the newly created view’s center to be at the given origin.

class ShapeView {
    let size: CGFloat = 150.0

    init(origin: CGPoint) {
        super.init(frame: CGRectMake(0.0, 0.0, size, size))        
        self.center = origin       
    }

    // We need to implement init(coder) to avoid compilation errors
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
...
}

We’ll draw a shape in our view in the drawRect: method. We’ll be using UIBezierPath for drawing. The drawRect:method does nothing by default, we override it to provide custom drawing logic for our view.

To draw a shape with UIBezierPath we first have to specify the geometry of our shape after with we use the fillmethod to fill the shape. To set the fill color we use the setFill method on UIColor. We’ll create our bezier path using the UIBezierPath(roundedRect:, cornerRadius:) init method. This will create a rounded rectangle shape. Afterwards we set the fill color and fill the path.

class ShapeView {
...
    override func drawRect(rect: CGRect) {

        let path = UIBezierPath(roundedRect: rect, cornerRadius: 10)

        UIColor.redColor().setFill()
        path.fill()
    }
}

Now back to the didTap method. We’ll create ShapeViews here and add them to the view hierarchy.

class ViewController {
...
    func didTap(tapGR: UITapGestureRecognizer) {

        let tapPoint = tapGR.locationInView(self.view)

        let shapeView = ShapeView(origin: tapPoint)

        self.view.addSubview(shapeView)
    }
...
}

Tapping on the screen will now add a red rounded rectangle at the tap location.

red rectangles

Notice that the rectangles have a black background near the corner region, to fix this problem we’ll set the ShapeView'sbackgroundColor to UIColor.clearColor() in the init method.

class ShapeView {
...
    init(origin: CGPoint) {
    ...
        self.backgroundColor = UIColor.clearColor()
    }
...
}

Stroked rectangles

In addition to provide a method for filling a path UIBezierPath also provides a method for stroking a path i.e. adding a colored outline to the path. One important aspect of how the stroke is drawn is that half of the stroke will be on the outside of the path and half of the stroke will be on the inside.

Lets add a stroke to our rectangles, first lets define the stroke’s width as a constant on our class lineWidth. The lineWidth can be set on a bezier path via it’s lineWidth property.

class ShapeView {
...
    let lineWidth: CGFloat = 3
...
}

Next we have to change our drawRect: method so that it strokes our path. Currently our shape is the size of our view, that means that half of our stroke would be outside the view and hence invisible. To fix this we’ll inset the rect we use to create our bezier path by half our line width. Insetting a CGRect is done via the CGRectInset(rect,dx,dy) function. The rect’s origin value is offset in the x-axis by the distance specified by the dx parameter and in the y-axis by the distance specified by the dy parameter, and its size adjusted by (2dx,2dy), relative to the source rectangle. If dx and dy are positive values, then the rectangle’s size is decreased.

We’ll also have to set a color for the stroke, this is done via the setStroke method on UIColor.

class ShapeView {
...
    override func drawRect(rect: CGRect) {

        let insetRect = CGRectInset(rect, lineWidth / 2, lineWidth / 2)

        let path = UIBezierPath(roundedRect: insetRect, cornerRadius: 10)


        UIColor.redColor().setFill()
        path.fill()

        path.lineWidth = self.lineWidth
        UIColor.blackColor().setStroke()
        path.stroke()
    }
}

Our rectangles also display a stroke now!

stroked rects

Panning

We’ll use a UIPanGestureRecognizer to move our shapes around. A UIPanGestureRecognizer is a gesture recognizer that detects panning i.e. moving a finger across the view. You can get the translation (amount of panning) of the gesture recognizer via the translationInView method. We’ll set the gesture recognizer’s translation back to 0 after the pan to simplify our logic.

One last thing to note is that whenever we’re interacting with a shape we’ll want to move it to the top of the view hierarchy so that it’s not covered up by some shape below it. To do this we call bringSubviewToFront(view) on the shape’s superview.

We’ll create a method initGestureRecognizers where we set up our gesture recognizers. Whenever a method gets too complex and starts doing multiple things it’s good design to split it into multiple methods. We’ll initialize ourUIPanGestureRecognizer just like we did we the UITapGestureRecognizer. In the target method we’ll implement the logic described above, effectively moving the view by adding the translation to it’s center property.

class ShapeView: UIView {
...
    init(origin: CGPoint) {
        ...
        initGestureRecognizers()
    }

    func initGestureRecognizers() {
        let panGR = UIPanGestureRecognizer(target: self, action: "didPan:")
        addGestureRecognizer(panGR)
    }

    func didPan(panGR: UIPanGestureRecognizer) {

        self.superview!.bringSubviewToFront(self)

        var translation = panGR.translationInView(self)

        self.center.x += translation.x
        self.center.y += translation.y

        panGR.setTranslation(CGPointZero, inView: self)
    }

...
}

We can move shapes around now!

Pinching

We’ll use a UIPinchGestureRecognizer to scale our shapes. A UIPinchGestureRecognizer is a gesture recognizer that detects pinching. When the user moves the two fingers toward each other, the meaning is zoom-out; when the user moves the two fingers away from each other, the meaning is zoom-in. You can get the scale of the gesture recognizer via the scale property. We’ll set the gesture recognizer’s scale back to 1 after the pinch to simplify our logic.

To scale a view we’ll modify the view’s transform property. The transform property is of type CGAffineTransform and can be used to modify a view’s scale, rotation, translation and more. See this for more details on affine transforms.

A view’s transform property is initially equal to CGAffineTransformIdentity with is the do nothing transform. We can use various methods to create a new transform or modify an existing one. In our case the best way is to modify the view’s transform via the CGAffineTransformScale(transform,scaleX,scaleY) function which takes a transform and multiplies its scale factor by scaleX in the horizontal direction and scaleY in the vertical direction. scaleX will be equal to scaleY and to the gesture recognizer’s scale property.

class ShapeView: UIView {
...

    func initGestureRecognizers() {
        let panGR = UIPanGestureRecognizer(target: self, action: "didPan:")
        addGestureRecognizer(panGR)

        let pinchGR = UIPinchGestureRecognizer(target: self, action: "didPinch:")
        addGestureRecognizer(pinchGR)
    }
...

    func didPinch(pinchGR: UIPinchGestureRecognizer) {

        self.superview!.bringSubviewToFront(self)

        let scale = pinchGR.scale

        self.transform = CGAffineTransformScale(self.transform, scale, scale)

        pinchGR.scale = 1.0
    }

...
}

We can scale shapes now!

Rotating

We’ll use a UIRotationGestureRecognizer to rotate our shapes. A UIRotationGestureRecognizer is a gesture recognizer that detects rotation. The gesture is triggered when the user moves the fingers opposite each other in a circular motion. You can get the rotation of the gesture recognizer via the rotation property. We’ll set the gesture recognizer’s rotation back to 0 after the rotation to simplify our logic. Rotation is given in radians.

To update the rotation of our view we’ll again modify the view’s transform property. We’ll use theCGAffineTransformRotate(transform,rotation) function to modify our view’s transform. The CGAffineTransformRotatefunction takes a transform and produces a new transform by adding rotation to the transforms rotation factor.

class ShapeView: UIView {
... 

    func initGestureRecognizers() {
        let panGR = UIPanGestureRecognizer(target: self, action: "didPan:")
        addGestureRecognizer(panGR)

        let pinchGR = UIPinchGestureRecognizer(target: self, action: "didPinch:")
        addGestureRecognizer(pinchGR)

        let rotationGR = UIRotationGestureRecognizer(target: self, action: "didRotate:")
        addGestureRecognizer(rotationGR)
    }
    ...
    func didRotate(rotationGR: UIRotationGestureRecognizer) {

        self.superview!.bringSubviewToFront(self)

        let rotation = rotationGR.rotation

        self.transform = CGAffineTransformRotate(self.transform, rotation)

        rotationGR.rotation = 0.0
    }

...
}

We can rotate shapes now!

Rotating and pinching a shape works fine now but we broke translation (When we try to move a scaled / rotated shape the movement won’t work correctly). There are multiple ways to fix it but easiest is to apply the affine transform of our view to the translation returned by the pan gesture recognizer.

class ShapeView {
...
    func didPan(panGR: UIPanGestureRecognizer) {

        self.superview!.bringSubviewToFront(self)

        var translation = panGR.translationInView(self)

        translation = CGPointApplyAffineTransform(translation, self.transform)

        self.center.x += translation.x
        self.center.y += translation.y

        panGR.setTranslation(CGPointZero, inView: self)
    }
...
}

All gestures should work fine now.

Random Colors

Lets improve the appearance of our rectangles by coloring them with a random color instead of red. We’ll create a random color using UIColor’s initWithHue:saturation:brightness:alpha: constructor.

HSB Explanation

The UIColor initWithHue:saturation:brightness:alpha: constructor creates a new color instance by specifying it’s hue, saturation, brightness and alpha values. All these values have to be floating point numbers between 0 and 1.
To get a feeling for how these numbers affect the resulting color open up a color picker and play with the sliders.

Color Picker

The above dark orange color can be created in code via:

UIColor(hue: 25.0 / 359.0, saturation: 0.8, brightness: 0.7, alpha: 1.0)

[collapse]

We’ll write a method that returns a random colors. First we’ll have to generate a random float between 0 and 1, this can be done via CGFloat(Float(arc4random()) / Float(UINT32_MAX)), arc4random() returns a value between 0 andUINT32_MAX so dividing that value by UINT32_MAX will give us a value between 0 and 1. For saturation and brightness we choose some arbitrary values. We’ll also make the alpha value 0.8, so that the shapes will be transparent.

class ShapeView {
...
    func randomColor() -> UIColor {
        let hue:CGFloat = CGFloat(Float(arc4random()) / Float(UINT32_MAX))
        return UIColor(hue: hue, saturation: 0.8, brightness: 1.0, alpha: 0.8)
    }
}

We’ll create a new property fillColor on ShapeView that we’ll initialize with a random color, we also have to modify the drawRect method to use our fillColor instead of UIColor.redColor().

class ShapeView {
...
   var fillColor: UIColor!

   init(origin: CGPoint) {

        super.init(frame: CGRectMake(0.0, 0.0, size, size))

        self.fillColor = randomColor()
    ...
    }
    ...

    override func drawRect(rect: CGRect) {
    ... 
        self.fillColor.setFill()
    ...
    }
...
}

Our rectangles will now appear slightly transparent with random colors. colored rects

More Shapes

It’s easy to add more type of shapes now that we have this going. Let’s add a circle and rectangle shape. We’re currently creating our path object in the drawRect: method. Let’s make path a property of the ShapeView class and initialize in the init method. For this we’ll write a method randomPath() -> UIBezierPath that returns a random path (a rectangle, a circle or a triangle).

The circle is easy to create, we just call the UIBezierPath(ovalInRect) constructor, the triangle is a bit trickier to create, you can find the exact details below.

class ShapeView {
...
    var path: UIBezierPath!
...
    func randomPath() -> UIBezierPath {

        let insetRect = CGRectInset(self.bounds,lineWidth,lineWidth)

        let shapeType = arc4random() % 3

        if shapeType == 0 {
            return UIBezierPath(roundedRect: insetRect, cornerRadius: 10.0)
        }

        if shapeType == 1 {
            return UIBezierPath(ovalInRect: insetRect)
        }
        return trianglePathInRect(insetRect)
    }

    init(origin: CGPoint) {
    ...  
        self.path = randomPath()
    ...   
    }

    override func drawRect(rect: CGRect) {            
        self.fillColor.setFill()
        self.path.fill()

        self.path.lineWidth = self.lineWidth
        UIColor.blackColor().setStroke()
        self.path.stroke()
    }
}

A triangle is a polygon with 3 vertices. UIBezierPath doesn’t have a constructor for creating a triangle but it has several methods that allow us to construct arbitrary polygons. To add a polygon to a bezier path you first have to add the first point via the moveToPoint(point) method. Lines can then be added to subsequent points via theaddLineToPoint(point) method. Once all the points are added, the polygon can be closed via the closePath method. This method connects the last added point to the initial point (specified in moveToPoint).

The 3 points on our triangle are: the midpoint at the top, the bottom right point and the bottom left point. We’ll create a method trianglePathInRect(rect) that returns a triangle path. This method is called in randomPath().

class ShapeView {
...
    func trianglePathInRect(rect:CGRect) -> UIBezierPath {
        let path = UIBezierPath()

        path.moveToPoint(CGPointMake(rect.width / 2.0, rect.origin.y))
        path.addLineToPoint(CGPointMake(rect.width,rect.height))
        path.addLineToPoint(CGPointMake(rect.origin.x,rect.height))
        path.closePath()


        return path
    }
...
}

Now instead of just rectangles we get a random path each time we tap the screen.

random paths

Download Source Code

Challenges

1 . Hatching

Add a hatching texture to the shapes. You can download 2 hatching images here. Hatching can be done by first filling the path with the fillColor and afterwards filling the path with a pattern image. You can create a color from an image using the UIColor(patternImage) constructor. Choose randomly between the 2 hatching images. Also randomly draw a shape with a hatching or not.

Hatching

Solution

class ShapeView {
    override func drawRect(rect: CGRect) {

        self.fillColor.setFill()

        self.path.fill()

        var name = "hatch"
        if arc4random() % 2 == 0 {
            name = "cross-hatch"
        }

        let color = UIColor(patternImage: UIImage(named: name)!)

        color.setFill()

        if arc4random() % 2 == 0 {
            path.fill()
        }

        UIColor.blackColor().setStroke()

        path.lineWidth = self.lineWidth

        path.stroke()
    }
}

[collapse]

Download Source Code

2 . Regular Polygons

Add a new shape. A regular polygon with a random number of vertices between 3..12. A regular polygon is a polygon that is equiangular (all angles are equal in measure) and equilateral (all sides have the same length).

Hint

You’ll have to connect N points on a circle. The angular offset between the points is constant and equal to 2 * PI / N. Create a method that returns a point on a circle at a certain, angle, radius and offset.

[collapse]

regular polygons

Solution

    func pointFrom(angle: CGFloat, radius: CGFloat, offset: CGPoint) -> CGPoint {
        return CGPointMake(radius * cos(angle) + offset.x, radius * sin(angle) + offset.y)
    }

    func regularPolygonInRect(rect:CGRect) -> UIBezierPath {
        let degree = arc4random() % 10 + 3

        let path = UIBezierPath()

        let center = CGPointMake(rect.width / 2.0, rect.height / 2.0)

        var angle:CGFloat = -CGFloat(M_PI / 2.0)
        let angleIncrement = CGFloat(M_PI * 2.0 / Double(degree))
        let radius = rect.width / 2.0

        path.moveToPoint(pointFrom(angle, radius: radius, offset: center))

        for i in 1...degree - 1 {
            angle += angleIncrement
            path.addLineToPoint(pointFrom(angle, radius: radius, offset: center))
        }

        path.closePath()

        return path
    }

[collapse]

Download Source Code

3 . Stars

Add a new star shaped path. It’s fun to create the star with a random number of corners 5…15 for example. The stars you end up with should look like this:

Stars

The problem of drawing a star is similar to drawing a regular polygon. First consider all the vertices on a regular polygon. Next for each pair of points consider a new point that has an angle halfway between them but has a lower radius. Connect all these points in order of their angle and you’ll end up with a star.

Star Explanation

Solution

    func starPathInRect(rect: CGRect) -> UIBezierPath {
        let path = UIBezierPath()

        let starExtrusion:CGFloat = 30.0

        let center = CGPointMake(rect.width / 2.0, rect.height / 2.0)

        let pointsOnStar = 5 + arc4random() % 10

        var angle:CGFloat = -CGFloat(M_PI / 2.0)
        let angleIncrement = CGFloat(M_PI * 2.0 / Double(pointsOnStar))
        let radius = rect.width / 2.0

        var firstPoint = true

        for i in 1...pointsOnStar {

            let point = pointFrom(angle, radius: radius, offset: center)
            let nextPoint = pointFrom(angle + angleIncrement, radius: radius, offset: center)
            let midPoint = pointFrom(angle + angleIncrement / 2.0, radius: starExtrusion, offset: center)

            if firstPoint {
                firstPoint = false
                path.moveToPoint(point)
            }

            path.addLineToPoint(midPoint)
            path.addLineToPoint(nextPoint)

            angle += angleIncrement
        }

        path.closePath()

        return path
    }

[collapse]

Download Source Code

4 . We❤Fractals

Add a simple fractal shape to the app. The shape is created as follows:
Step 1: Add a circle at some position

Step 2: Add a circle at each of the vertices of a regular polygon inscribed in that circle

Step 3: Repeat step 2 for each circle.

Iterating this algorithm 6 times gives us a shape that looks similar to Sierpinski’s triangle

Fractal

Solution

    func addDetailToFractalPath(center: CGPoint, radius: CGFloat, path:UIBezierPath, iterations:Int) {
        if iterations == 0 {
            return
        }

        var angle:CGFloat = -CGFloat(M_PI / 2.0)
        let angleIncrement = CGFloat(M_PI * 2.0 / Double(3))

        path.appendPath(UIBezierPath(ovalInRect: CGRectMake(center.x - radius, center.y - radius, radius * 2, radius * 2)))

        for i in 1...3 {

            let point = pointFrom(angle, radius: radius, offset: center)



            addDetailToFractalPath(point, radius: radius / 2.0, path: path, iterations: iterations - 1)

            angle += angleIncrement
        }

        path.closePath()
    }

    func fractalPathInRect(rect: CGRect) -> UIBezierPath {
        let path: UIBezierPath = UIBezierPath()

        addDetailToFractalPath(CGPointMake(rect.size.width / 2, rect.size.height / 2), radius: 35.0, path: path, iterations: 5)

        return path
    }

[collapse]

  9 comments for “Bezier Paths and Gesture Recognizers

  1. John Doe
    July 29, 2015 at 1:23 pm

    Love this tutorial. I was wondering if there is a way to detect a using drawing certain objects. For example tell the user to draw a circle and then press a button. The button will detect if a circle is drawn in the area.

    • July 29, 2015 at 2:16 pm

      Hi, I’ve been thinking about this problem yesterday :), I’m considering writing a tutorial on the topic.

      The problem of recognizing general shapes is pretty complex. What you want to do is implement a custom UIGestureRecognizer and record all of the touch points that occur between the TouchesBegan and TouchedEnded states. Once you have all of those touches it’s time to analyze them. I think you can get away with some heuristics for simple shapes. For example to detect a circle you could take the center of gravity of all the touches and compute the distance from each touch to the center of gravity. Now depending on these distances you can classify the object as being a circle. The touches should have the same distance to the center of gravity up to some error. Also the touches should be uniformly distributed in terms of their angle to the center of gravity.

      Note that I haven’t implemented this, so it’s mostly guess work at this point.

      Another approach would be to use a neural network. I think this approach will give better results than the above heuristic. Neural networks can detect a vast number of shapes, you just have to feed the neural network data and it will figure out how to do the shape classification.

  2. vinicius
    September 3, 2015 at 1:05 pm

    Hi,
    Can I use it to draw a big number of circles? I want to represent boreholes in a terrain (topview), but in general I have 400 or 500 boreholes… my ideia is draw inside a view circles (proportionally), but I don’t know if the quantity of shapes can make the app be slow.

    regards

    • September 3, 2015 at 2:25 pm

      Yup, this approach can definitely handle 500+ circles on a modern device. If you’re also targeting older iPads chances are you’ll run into performance issues.

  3. Shawn
    September 8, 2015 at 6:09 pm

    Thank you so much! This has been extremely helpful to me.

  4. Dave
    March 20, 2016 at 5:41 am

    hi, I’ m early in the tutorial, where I’ve just added the pinch gesture recognizer… There is an odd bug, where if I resize the red square, then try to pan it, the panning no longer matches the object. it’s as if the scale didn’t apply to the pans, so if i repeatedly resize it, the pans get to be quite disassociated with the object.

    for example if i make it much larger, then try to pan it, it seems to follow my pan at a proportion to 1/scale… what might I have done wrong?

    • Dave
      March 20, 2016 at 4:33 pm

      well… i figured out that the translation needs to be based on the superview of the shape view…. but i’ve come across several other bugs while I was at it… if you pinch to zoom, the center of the pinch is not where your fingers are, likely because it’s based on the coordinate system of the view and not the superview again… that can get really bad when it’s been made a large size. also, when you make the object very large, it’s totally pixelated… that’s because it’s not actually redrawing and re-laying out the objects… it’s just drawing small and scaling the bitmap… oh well… i guess technically the tutorial is about fixing those kinds of bugs…

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe
We send about one email per week with our latest tutorials and updates
Never display this again :)