Creating iOS Custom Views in UIKit

ProfilePicture of Andres Canal
Andres Canal
Senior Full-stack Mobile Developer
Mobile phone with an UIKIT Custom View example

I first started working with iOS fresh out of university. Working with an unfamiliar platform and programming language was challenging. What really made me nervous, was the designs coming from the creative team. I know a great design can breathe new life into a website, but unique high concept designs often require the creation of iOS custom views in UIKit.

As a new developer (that was me at the time!), this can be pretty daunting. So the aim of this article is to help any developer  convert designs into a functional user-interface elements.

iOS Custom View

Views are the fundamental building blocks of your app’s user interface. Normally, you create views in your storyboards by dragging them from the library to your canvas. But, sometimes you need to create an element that is not available using the standard ‘label’ or ‘button’ elements in UIKit. This is when you need a custom view.

By the way, if any of the terminology in this article sounds unfamiliar, you might want to check out Apple’s UIKit documentation. From now on, I’ll just assume you have a working knowledge of UIKit and Swift.

The Initial Design

Let’s start by picking a design that cannot be recreated within the standard UIKit view. I found this Circular Progress Bar, designed by Geng Gao, which fits the bill perfectly.

Circular Progress Bar UIKIT Custom View

This element is composed of two text labels (the title and subtitle) and a circular completion indicator that is intended to fill a gray track as the task progresses. We’ll start by creating a new Xcode project and naming it CircularProgressBar.

File > New > Project > Single View Application

Screenshot of project creation in Xcode

As we will be creating a custom UIView, we need to extend UIKit’s default UIView with a new class: CircularProgressBar.

1import UIKit
2class CircularProgressBar: UIView {
3}
4

There are two ways of adding labels to our design: by code or with a .xib file.

To avoid any potential confusion further down the line, the terms ‘xib’ and ‘nib’ are often used interchangeably. NIB comes from ‘NeXTSTEP Interface Builder’,  Apple’s now discontinued OS. While .nib files have been replaced with .xib files, developers still refer to them as ‘nibs’.

I like to create my iOS custom views using .xib files because they require less coding and are easier to make changes to. So let’s do that and also name it CircularProgressBar.

File > New > File and select View

Screenshot of xcode window for creating a custom view

We will select CircularProgressBar.xib in the navigator, and then define the file owner for the .xib to our class extension: CircularProgressBar

Screenshot of changing view file with new class extension in Xcode

With that understood, we will hide the status bar and set the size of the .xib to ‘freeform’. This way we can change the dimensions so the view has a similar size to the design. In this case 300 x 300 pixels.

Screenshot of Xcode size setup for a custom view

For clarity, let’s make the background red so it stands out. We’ll also add the title and subtitle labels.

Screenshot of backgruond setup for custom view

We then need to write the following code so CircularProgressBar loads the .xib file we just created:

1import UIKit
2
3open class CircularProgressBar: UIView {
4    var view: UIView!
5    required public init?(coder aDecoder: NSCoder) {
6        super.init(coder: aDecoder)
7        loadViewFromNib()
8    }
9
10    override init(frame: CGRect) {
11        super.init(frame: frame)
12        loadViewFromNib()
13    }
14
15    func loadViewFromNib() {
16        let bundle = Bundle(for: type(of: self))
17        let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
18        let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
19        view.frame = bounds
20        view.autoresizingMask = [
21            UIViewAutoresizing.flexibleWidth,
22            UIViewAutoresizing.flexibleHeight
23        ]
24        addSubview(view)
25        self.view = view
26    }
27}
28

Next, mark the UIView as @IBDesignable and add it to the Main.storyboard to see how it renders. If you are not sure what IBDesignable is, take a look at this awesome post from NSHipster.

To add CircularProgressBar to the ViewController in the Main.storyboard we have to add a standard UIView and then change its class to CircularProgressBar. If everything goes well, you should see your custom view rendered in the ViewController:

Screenshot of custom view rendered in the Xcode ViewController

You can always view the commit on our repository labeled: Stage 1: Rendering view in the interface builder.

Creating CALayers

So far, we have created a custom UIView that loads a .xib file and renders in the interface builder. Now we start getting into the cool stuff: Core Animation layers.

CALayer is shorthand for Core Animation: a framework that provides all the tools required to render graphics and animations. This huge framework can sometimes be overwhelming and impractical so Apple built the simpler UIKit on top of it. UIKit is an easy way to create simple views like UILabels, UITextViews, etc.

When creating custom designs we need to leverage the power of the Cora Animation framework through CALayers. Let’s start by simply drawing the circumference of a circle. Create a new layer and name it BorderLayer.

File > New > File > Cocoa Touch Class

Screenshot of Xcode CALayers creation

Let’s add a new layer to CircularProgressBar. To do this, we will create a method called commonInit() and call it right after loadViewFromNib() in our initializers. In commonInit() create a new instance of our BorderLayer and add it to the layer of our view.

1open func commonInit() {
2 let borderLayer = BorderLayer()
3 self.layer.addSublayer(borderLayer)
4 }
5

If you run the app, you’ll see that nothing appears to have changed. The layer is there, but we are not able to see it because it has no size, no color and no drawings on it. So let’s add in these properties!

1. Create an instance of BorderLayer and store it in a constant called ‘darkBorderLayer’.

2. Add ‘darkBorderLayer’to the view’s layer.

3. This will override layoutSubviews(). This method is called when all the sizes have been resolved, the size of your view is going to be its final size. So this is the perfect time to set the size of your layer.

4. Call setNeedsDisplay() to notify the system that the content of the layer needs to be redrawn.

FYI: Each of the above steps is commented in the snippet below.

1open class CircularProgressBar: UIView {
2    var view: UIView!
3    // 1
4    let darkBorderLayer = BorderLayer()
5    .
6    .
7    .
8    // 2
9    open func commonInit() {
10        self.layer.addSublayer(darkBorderLayer)
11    }
12    // 3
13    override open func layoutSubviews() {
14        super.layoutSubviews()
15        darkBorderLayer.frame = self.bounds
16    // 4
17        darkBorderLayer.setNeedsDisplay()
18    }
19}
20

There is just one thing missing: actually drawing something in the BorderLayer! This is done by overriding the method draw(in ctx: CGContext). I like to think of CGContext as a whiteboard where I can make my drawings.

1class BorderLayer: CALayer {
2    override func draw(in ctx: CGContext) {
3        let lineWidth:CGFloat = 2.0
4        let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
5        ctx.beginPath()
6        ctx.setStrokeColor(UIColor.blue.cgColor)
7        ctx.setLineWidth(lineWidth)
8        ctx.addArc(
9            center: center,
10            radius: bounds.height/2 - lineWidth,
11            startAngle: 0,
12            endAngle: 2.0 * CGFloat.pi,
13            clockwise: false
14        )
15        ctx.drawPath(using: .stroke)
16    }
17}
18

When you run the app, you should see two labels surrounded by a blue circle.

Screenshot of the app running in a iOS device

Don’t worry if things don’t look quite right. You can view everything we’ve done up to this point in our repository. The commit is called: Stage 2: Adding our first layer to the view.

The original design consists of two overlapping circles: a gray circle underneath a green one. It is the latter which shows the task progress. To achieve this effect, we need to create one more layer and store it in progressBorderLayer. By the way, you can add as many layers as you want. You just need to keep in mind that layers are stacked so the most recent label will cover any previous ones.

Lastly, we will refactor the BorderLayer so we can set the color, size, start angle and end angle from outside of the class:

1import UIKit
2
3class BorderLayer: CALayer {
4    var lineColor: CGColor = UIColor.blue.cgColor
5    var lineWidth: CGFloat = 2.0
6    var startAngle: CGFloat = 0.0
7    @NSManaged var endAngle: CGFloat = 0.0
8    override func draw(in ctx: CGContext) {
9        let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
10        ctx.beginPath()
11        ctx.setStrokeColor(lineColor)
12        ctx.setLineWidth(lineWidth)
13        ctx.addArc(
14            center: center,
15            radius: bounds.height/2 - lineWidth,
16            startAngle: startAngle,
17            endAngle: endAngle,
18            clockwise: false
19        )
20        ctx.drawPath(using: .stroke)
21    }
22}
23

In Swift, @NSManaged is how you tell the compiler that this is actually a @dynamic objective-c variable. Essentially, @dynamic tells Core Animation to track the property changes and then call different methods from our layers. A deeper explanation is outside the scope of this article – so you’ll just have to trust me!

Now let’s add the progressBorderLayer to our view and set the correct colors used in design template.

1import UIKit
2
3@IBDesignable
4
5open class CircularProgressBar: UIView {
6    var view: UIView!
7    let darkBorderLayer: BorderLayer!
8    var progressBorderLayer: BorderLayer!
9    .
10    .
11    .
12    override open func layoutSubviews() {
13        super.layoutSubviews()
14        darkBorderLayer.frame = self.bounds
15        progressBorderLayer.frame = self.bounds
16        progressBorderLayer.setNeedsDisplay()
17        darkBorderLayer.setNeedsDisplay()
18    }
19
20    open func commonInit() {
21        darkBorderLayer = BorderLayer()
22        darkBorderLayer.lineColor = UIColor(
23            red: 134/255,
24            green: 133/255,
25            blue: 148/255,
26            alpha: 1
27        ).cgColor
28        darkBorderLayer.startAngle = 0
29        darkBorderLayer.endAngle = 2.0 * CGFloat.pi
30        self.layer.addSublayer(darkBorderLayer)
31        progressBorderLayer = BorderLayer()
32        progressBorderLayer.lineColor = UIColor(
33            red: 168/255,
34            green: 207/255,
35            blue: 45/255,
36            alpha: 1
37        ).cgColor
38        progressBorderLayer.startAngle = 0
39        progressBorderLayer.endAngle = CGFloat.pi
40        self.layer.addSublayer(progressBorderLayer)
41    }
42}
43

After matching the background color and fonts, you should see things are really starting to come together.

Screenshot of the app complete running in a iOS device

Now we’ll create @IBInspectable variables for the title and subtitle and then change their values in the interface builder.

1@IBOutlet weak var titleLabel: UILabel!
2
3    @IBOutlet weak var subtitleLabel: UILabel!
4
5    @IBInspectable var title: String = "" {
6        didSet {
7            titleLabel.text = title
8        }
9    }
10
11    @IBInspectable var subtitle: String = "" {
12        didSet {
13            subtitleLabel.text = subtitle
14        }
15    }
16

I’m creating another commit here so you can have everything we have done up to this point. The commit is called: Stage 3: Adding the second layer and applying styles.

Updating the Progress Bar

Nearly there! We’re going to change the value of the progress bar in our view. This is done by updating the green line layer. We’re also going to add in a UISlider to the main storyboard, so we can test the progress indicators behavior.

Connect the UISlider to a ‘Value Changed’ action and set the minimum and maximum values for the slider to 0 and 100. I marked in the image where we want the green line to start filling the gray track: that’s our 0 value.

Screenshot of progress bar setup for app in Xcode

As you may have noticed, in BorderLayer we are using a method called addArc to draw the border of the circle. This method receives two parameters that we need to pay special attention to: the startAngle and endAngle. You can likely take a guess at what they do from their name, but it’s important to know that they receive their values as radians. Radians are just a way of measuring angles. To work with them, we need to build a function that receives a number and returns the equivalent value of that number as a radian. This function will transform, for example, 30% to its equivalent radian. Once we have the radian value we can use it in addArc to draw the arc:

1static let startAngle = 3/2 * CGFloat.pi
2
3static let endAngle = 7/2 * CGFloat.pi
4
5internal class func radianForValue(_ value: CGFloat) -> CGFloat {
6        let realValue = CircularProgressBar.sanitizeValue(value)
7        return (realValue * 4/2 * CGFloat.pi / 100) + CircularProgressBar.startAngle
8    }
9
10
11internal class func sanitizeValue(_ value: CGFloat) -> CGFloat {
12        var realValue = value
13        if value < 0 {
14            realValue = 0
15        } else if value > 100 {
16            realValue = 100
17        }
18        return realValue
19    }
20

We are also going to change the startAngle and endAngle of our layers using our two new constants called startAngle and endAngle. Go to the commonInit() method in CircularProgressBar and change the angles.

1open func commonInit() {
2        darkBorderLayer.lineColor = UIColor(
3            red: 134/255,
4            green: 133/255,
5            blue: 148/255,
6            alpha: 1
7        ).cgColor
8        darkBorderLayer.startAngle = CircularProgressBar.startAngle
9        darkBorderLayer.endAngle = CircularProgressBar.endAngle
10        self.layer.addSublayer(darkBorderLayer)
11        progressBorderLayer.lineColor = UIColor(
12            red: 168/255,
13            green: 207/255,
14            blue: 45/255,
15            alpha: 1
16        ).cgColor
17        progressBorderLayer.startAngle = CircularProgressBar.startAngle
18        progressBorderLayer.endAngle = CircularProgressBar.endAngle
19        self.layer.addSublayer(progressBorderLayer)
20    }
21

So let’s recap: we have our slider, a function to translate a CGFloat value to a radian and our view. Now, all we need is to update the endAngle property of our progressBorderLayer:

1@IBInspectable var progress: CGFloat = 0.0 {
2
3    didSet {
4      progressBorderLayer.endAngle = CircularProgressBar.radianForValue(progress)
5    }
6}
7
8

The above creates an @IBInspectable variable called progress. We are going to use the didSet observer, that is called immediately after the new value is stored, to update our layer’s endAngle.

The UISlider should update the progress property of our view. To do this, create an @IBOutlet variable for CircularProgress bar and connect it to the main Main.storyboard view. Name it circularProgressBar. We will then create a function that we will call every time the user moves the slider. The function receives as a parameter the UISlider object with the current value (between 0 and 100). That value is then sent to the progress property in our view.

1@IBOutlet weak var circularProgressBar: CircularProgressBar!
2
3@IBAction func sliderAction(_ sender: UISlider) {
4    self.circularProgressBar.progress = CGFloat(sender.value)
5}
6

CALayers are lazy, and they don’t like to redraw themselves. This is a built-in optimization to avoid redrawing a layer if no property of the layer has changed. So, if we want progressBorderLayer to redraw everytime the endAngle property is updated we need to add the following method to BorderLayer:

1    override class func needsDisplay(forKey key: String) -> Bool {
2        if key == "endAngle" {
3            return true
4        }
5        return super.needsDisplay(forKey: key)
6    }
7
8

This function simply says ‘if the endAngle property changes then we need to update the layer and redraw is going to be called’.

Everything is set now so let’s run the app!

iOS Custom Views Slider Demo

I created a final commit to our project repository: Stage 4: Updating the progress.

Conclusion

Hopefully, this tutorial has given you the tools and know-how to create your own iOS custom views in UIKit.

Loading an .xib file in a UIView extension is in itself a great timesaver. It allows you to compose your view using the UI tools you’re already familiar with, while still keeping view logic in a custom class. Then, by keeping the logic for connecting user interaction to your view’s properties in a view controller, you improve your app’s maintainability and are able to reuse your custom views anywhere.

If you are looking for more content for iOS developers, we created an article about the difference between Carthage vs Cocoapods, and a complete guide of iOS unit testing, to nail your next app testing.


Looking to hire?

Join our newsletter

Join thousands of subscribers already getting our original articles about software design and development. You will not receive any spam. just great content once a month.

 

Read Next

Browse Our Blog