How to make polygon image mask

● 14 Feb 2015

We all as iOS developer must have used cornerRadius property in CALayer to make our UIImageView to be round or circle according to the design from designer. But what if the designer want someting more than that, polygon? star?, what should we do.

This blog will show you how to use UIBezierPath to make a dynamic polygon to be mask for UIImageView like this

Create Path

Let’s create new swift playground file so we can see the updates when we made changes to the code and then create the custom UIView subclass to show our path in drawRect() method.

import UIKit
import Darwin

class CustomView: UIView {
    
    override func drawRect(rect: CGRect) {
        // draw bezier here
    }
    
}

let view = CustomView(frame: CGRectMake(0, 0, 400, 400))

1. Declare the constants.

Put the following lines to our class.

let count = 8
let inset: CGFloat = 10
let lineWidth: CGFloat = 10
let lineColor = UIColor.lightGrayColor()

count is the number of angles in polygon.
inset is the view padding size.
lineWidth is the size of the path line.

And put the following lines to drawRect()

let insetRect = CGRectInset(rect, inset, inset)
let radius = (insetRect.width > insetRect.height ? insetRect.height : insetRect.width) / 2.0
let center = CGPoint(x: CGRectGetMidX(insetRect), y: CGRectGetMidY(insetRect))
let radian = CGFloat(M_PI * 2) / CGFloat(count)

radius because our polygon base on shape of circle, so we need to know the radius.
center center of the view and circle.
radian the radian value per angle. The complete circle need 2π radian, so the radian per angle will be 2π / number of angle.

2. Create a bezier path

Put the following lines to drawRect:

let path = UIBezierPath()
path.lineWidth = lineWidth
lineColor.setStroke()

Then move the start point to middle top using center.y value by substract with radius.

let startPoint = CGPointMake(center.x, center.y - radius)
path.moveToPoint(startPoint)

Now we are at the red dot.

Next, we need to draw lines to make polygon. Let’s start with first destination.
In order to draw a line, we need point (x,y) and angle (θ). According to the Apple document, Angles in default coordinate is:

So angle (θ) is equal to:

let θ = CGFloat(3 * M_PI_2) - radian

and the point (x,y) is equal to:

let point = CGPoint(
    x: center.x + radius * cos(θ),
    y: center.y + radius * sin(θ)
)

As you can see in the following image.

Let’s use those values in the for-loop and draw path!

for i in 1...count  {
    let θ = CGFloat(3* M_PI_2) - CGFloat(i) * radian
            
    let point = CGPoint(
        x: center.x + radius * cos(θ),
        y: center.y + radius * sin(θ)
    )
    
    path.addLineToPoint(point)
}

path.stroke()

See the result.

But something is missing, yeah the curve at every corners.

3. Add curves

Back to starting point where we was at red dot. We need to draw curves that are a part of circle, so imagine we have small circle inside at every angle.

Because we are at the center and top of the circles, we will add first curve with half radian (every small circle also use same radiant value as bigger one because total radian will be equal to 2π or 360 degree) and in order to add curve we need the control point which is the center of the small circle.

Add constants as following lines before for-loop

let subRadius = radius * 0.4
let firstControlPoint = CGPoint(x: startPoint.x, y: startPoint.y + subRadius)

You can adjust subRadius ratio multiplier if you want the angle to have more curve or sharp. Put the following lines above for-loop to add a curve into our path before we got into the loop.

path.addArcWithCenter(firstControlPoint,
    radius: subRadius,
    startAngle: CGFloat(3 * M_PI_2),
    endAngle: CGFloat(3 * M_PI_2) - radian / 2,
    clockwise: false)

As you can see in the following image.

Let’s go into for-loop for the rest of remaining small circles. After declaration of θ and point put these lines to find the control point for particular angle and we have to use 0.6 as multiplier for radius because we used 0.4 for subRadius

let controlPoint = CGPoint(
    x: center.x + (radius * 0.6) * cos(θ),
    y: center.y + (radius * 0.6) * sin(θ)
)

Also we need to know radian of the point that touch the small circle by use atan2 function. The function take 2 arguments which is distance of point y and x to the center of circle and return the radian. You can read more about it at http://en.wikipedia.org/wiki/Atan2

let deltaY = point.y - controlPoint.y
let deltaX = point.x - controlPoint.x
let radianDelta = atan2(deltaY, deltaX)

Then we need to find angle that the curve will start and end with using the radian that we got from atan2.

var startAngle = radianDelta + (radian / 2)
var endAngle = radianDelta - (radian / 2)

Remove path.addLineToPoint(point) we will use addArcWithCenter() again. It will automatically draw a line if current point is not in the curve of control point, so we do not need to draw straight line.

path.addArcWithCenter(controlPoint,
    radius: subRadius,
    startAngle: startAngle,
    endAngle: endAngle, clockwise: false)

As you can see in the following image.

See the result.

4. Apply mask to UIImageView

Now we have a working polygon bezier path, next we need to create subclass of UIView to wrap the UIImageView inside and use the path to do a mask by following code.

let maskLayer = CAShapeLayer()
maskLayer.path = path.CGPath
imageView.layer.mask = maskLayer

Conclusion

We can use this to create many styles of UIImageView by change the value of count or even re-write the path to make star or what ever you want. Do not stick with the square only. You can see the complete source code at Github.

comments powered by Disqus