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 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. 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.
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
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
We will select CircularProgressBar.xib in the navigator, and then define the file owner for the .xib to our class extension: CircularProgressBar
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.
For clarity, let’s make the background red so it stands out. We’ll also add the title and subtitle labels.
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:
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
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.
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.
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.
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!
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.
Are you searching for your next programming challenge?
Scalable Path is always on the lookout for top-notch talent. Apply today and start working with great clients from around the world!