Creating iOS Custom Views in UIKit


iOS Custom View Blog Header

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 though; were the designs that would come through from the creative team. A great design can breathe new life into a website, but the more unique it is, the more likely it will require the creation of iOS custom views in UIKit. As a new developer, this can be pretty daunting.

The aim of this article is to explain how you can convert any design into a functional user-interface element.

iOS Custom Views

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. I’ll be assuming you have a working knowledge of UIKit and Swift for the rest of this article.

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

iOS Custom Views Screenshot

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

import UIKit
class CircularProgressBar: UIView {
}

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

iOS Custom View Screenshot

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

iOS Custom View Screenshot

Wait, what’s a file owner? StackOverflow supplies a more elegant explanation than I ever could: “The File Owner is an instantiated, runtime object that owns the contents of your .nib and its outlets/actions when the .nib is loaded. It can be an instance of any class you like.”

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.

iOS Custom Views Screenshot

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

iOS Custom Views Screenshot

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

import UIKit

open class CircularProgressBar: UIView {
    var view: UIView!
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        loadViewFromNib()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        loadViewFromNib()
    }

    func loadViewFromNib() {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
        let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
        view.frame = bounds
        view.autoresizingMask = [
            UIViewAutoresizing.flexibleWidth,
            UIViewAutoresizing.flexibleHeight
        ]
        addSubview(view)
        self.view = view
    }
}

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:

iOS Custom Views screenshot

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

iOS Custom Views Screenshot

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.

open func commonInit() {
        let borderLayer = BorderLayer()
        self.layer.addSublayer(borderLayer)
    }

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.

open class CircularProgressBar: UIView {
    var view: UIView!
    // 1
    let darkBorderLayer = BorderLayer()
    .
    .
    .
    // 2
    open func commonInit() {
        self.layer.addSublayer(darkBorderLayer)
    }
    // 3
    override open func layoutSubviews() {
        super.layoutSubviews()
        darkBorderLayer.frame = self.bounds
    // 4
        darkBorderLayer.setNeedsDisplay()
    }
}

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.

class BorderLayer: CALayer {
    override func draw(in ctx: CGContext) {
        let lineWidth:CGFloat = 2.0
        let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
        ctx.beginPath()
        ctx.setStrokeColor(UIColor.blue.cgColor)
        ctx.setLineWidth(lineWidth)
        ctx.addArc(
            center: center,
            radius: bounds.height/2 - lineWidth,
            startAngle: 0,
            endAngle: 2.0 * CGFloat.pi,
            clockwise: false
        )
        ctx.drawPath(using: .stroke)
    }
}

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

iOS Custom Views Screenshot

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:

import UIKit

class BorderLayer: CALayer {
    var lineColor: CGColor = UIColor.blue.cgColor
    var lineWidth: CGFloat = 2.0
    var startAngle: CGFloat = 0.0
    @NSManaged var endAngle: CGFloat = 0.0
    override func draw(in ctx: CGContext) {
        let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
        ctx.beginPath()
        ctx.setStrokeColor(lineColor)
        ctx.setLineWidth(lineWidth)
        ctx.addArc(
            center: center,
            radius: bounds.height/2 - lineWidth,
            startAngle: startAngle,
            endAngle: endAngle,
            clockwise: false
        )
        ctx.drawPath(using: .stroke)
    }
}

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.

import UIKit

@IBDesignable

open class CircularProgressBar: UIView {
    var view: UIView!
    let darkBorderLayer: BorderLayer!
    var progressBorderLayer: BorderLayer!
    .
    .
    .
    override open func layoutSubviews() {
        super.layoutSubviews()
        darkBorderLayer.frame = self.bounds
        progressBorderLayer.frame = self.bounds
        progressBorderLayer.setNeedsDisplay()
        darkBorderLayer.setNeedsDisplay()
    }

    open func commonInit() {
        darkBorderLayer = BorderLayer()
        darkBorderLayer.lineColor = UIColor(
            red: 134/255,
            green: 133/255,
            blue: 148/255,
            alpha: 1
        ).cgColor
        darkBorderLayer.startAngle = 0
        darkBorderLayer.endAngle = 2.0 * CGFloat.pi
        self.layer.addSublayer(darkBorderLayer)
        progressBorderLayer = BorderLayer()
        progressBorderLayer.lineColor = UIColor(
            red: 168/255,
            green: 207/255,
            blue: 45/255,
            alpha: 1
        ).cgColor
        progressBorderLayer.startAngle = 0
        progressBorderLayer.endAngle = CGFloat.pi
        self.layer.addSublayer(progressBorderLayer)
    }
}

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

iOS Custom Views Screenshot

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

    @IBOutlet weak var titleLabel: UILabel!

    @IBOutlet weak var subtitleLabel: UILabel!

    @IBInspectable var title: String = "" {
        didSet {
            titleLabel.text = title
        }
    }

    @IBInspectable var subtitle: String = "" {
        didSet {
            subtitleLabel.text = subtitle
        }
    }

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.

iOS Custom Views Screenshot

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:

static let startAngle = 3/2 * CGFloat.pi

static let endAngle = 7/2 * CGFloat.pi

internal class func radianForValue(_ value: CGFloat) -> CGFloat {
        let realValue = CircularProgressBar.sanitizeValue(value)
        return (realValue * 4/2 * CGFloat.pi / 100) + CircularProgressBar.startAngle
    }


internal class func sanitizeValue(_ value: CGFloat) -> CGFloat {
        var realValue = value
        if value < 0 {
            realValue = 0
        } else if value > 100 {
            realValue = 100
        }
        return realValue
    }

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.

open func commonInit() {
        darkBorderLayer.lineColor = UIColor(
            red: 134/255,
            green: 133/255,
            blue: 148/255,
            alpha: 1
        ).cgColor
        darkBorderLayer.startAngle = CircularProgressBar.startAngle
        darkBorderLayer.endAngle = CircularProgressBar.endAngle
        self.layer.addSublayer(darkBorderLayer)
        progressBorderLayer.lineColor = UIColor(
            red: 168/255,
            green: 207/255,
            blue: 45/255,
            alpha: 1
        ).cgColor
        progressBorderLayer.startAngle = CircularProgressBar.startAngle
        progressBorderLayer.endAngle = CircularProgressBar.endAngle
        self.layer.addSublayer(progressBorderLayer)
    }

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:

@IBInspectable var progress: CGFloat = 0.0 {

    didSet {
      progressBorderLayer.endAngle = CircularProgressBar.radianForValue(progress)
    }
}

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.

@IBOutlet weak var circularProgressBar: CircularProgressBar!

@IBAction func sliderAction(_ sender: UISlider) {
    self.circularProgressBar.progress = CGFloat(sender.value)
}

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:

    override class func needsDisplay(forKey key: String) -> Bool {
        if key == "endAngle" {
            return true
        }
        return super.needsDisplay(forKey: key)
    }

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.