Creating Custom Progress Bar in iOS using Swift

As I started developing my own iOS app, often I faced the need to customize certain built-in UI elements to suit the application’s requirements. One such for me was to customize a progress bar.

In this article, I will go through the basics of building a custom progress bar. First, understanding of the built-in UIProgressView helped. When I say understanding, I mean just the key properties such as Progress tint, Track tint and Progress value shown in the Attributes inspector of XCode.

  • Progress tint — used in the foreground layer to show the progress.
  • Track tint — used in the background layer to show the outline of the progress bar.
  • Progress value — a pointer value that maintains the current state of progress

Lets get into building the custom progress bar part. The requirement is to create a circular progress bar.

Custom Progress Bar

3 steps to create the custom progress bar

  • Creating a path using UIBezierPath() in a custom view.
  • Creating two CAShapeLayer — one for foreground and another for background.
  • Setting the progress by manipulating the StrokeEnd value.

The class declaration would look like:

class ProgressBarView: UIView { 
var bgPath: UIBezierPath!
var shapeLayer: CAShapeLayer!
var progressLayer: CAShapeLayer!
}

Step 1:

UIBeizerPath lets you create anything from simple paths to complex polygons by adding a set of lines, arcs, curves and rendering it into a custom view. So first define the path of the progress bar you need using this. For our example, I have created a circular path.

private func createCirclePath() { 
let x = self.frame.width/2
let y = self.frame.height/2
let center = CGPoint(x: x, y: y)
    bgPath.addArc(withCenter: center, radius: x/CGFloat(2), startAngle: CGFloat(0), endAngle: CGFloat(6.28), clockwise: true)
    bgPath.close() 
}

I have used addArc() method to draw a circle for which startAngle and endAngle parameter are specified as radians. Other methods like addLine(), addCurve(), addQuadCurve() can also be used to create a path. Make sure to close the path by calling the close() method.

Step 2:

CAShapeLayer: I like to think of this as similar to layers in photoshop. CAShapeLayer is useful in many ways because the objects are stored in vector-form and it has animation and transaction applicability.

For my requirement, I have created twoCAShapeLayer and formatted it to suit the needs.

func simpleShape() { 
createCirclePath()
    shapeLayer = CAShapeLayer()
shapeLayer.path = bgPath.cgPath
shapeLayer.lineWidth = 15
shapeLayer.fillColor = nil
shapeLayer.strokeColor = UIColor.lightGray.cgColor
    progressLayer = CAShapeLayer() 
progressLayer.path = bgPath.cgPath
progressLayer.lineWidth = 15
progressLayer.lineCap = kCALineCapRound
progressLayer.fillColor = nil
progressLayer.strokeColor = UIColor.red.cgColor
progressLayer.strokeEnd = 0.0
    self.layer.addSublayer(shapeLayer)
self.layer.addSublayer(progressLayer)
}

shapeLayer is the background layer and progressLayer is the foreground layer. Notice that both the layers are set to use the bgPath created in step 1 to give the effect of circular progression. We can also adjust properties like lineWidth, fillColor, strokeColor, strokeStart, strokeEnd, fillRule, lineCap, etc.

Step 3:

Create a variable progress that stores the current state of progress.

var progress: Float = 0 { 
willSet(newValue)
{
progressLayer.strokeEnd = CGFloat(newValue)
}
}

Notice in step 2, for the progressLayer , the attribute strokeEnd is set to 0.0. This is where the magic is. The strokeEnd can take values from 0.0 to 1.0 representing the start and end value respectively. Update the value for strokeEnd using willSet observer property. willSet observes for change in value of a variable and will set the new value to it.

And its done! Open main.storyboard , create a view and in its Identity inspector, specify the custom class as created ProgressBarView class.

Now from ViewController , using a scheduledTimer based on the durationTime, I have incremented the progressValue. You can manipulate this value based on your needs.

class ViewController: UIViewController { 
@IBOutlet weak var progressBar: ProgressBarView! 
var timer: Timer!
var progressCounter:Float = 0
let duration:Float = 10.0
var progressIncrement:Float = 0
override func viewDidLoad() { 
super.viewDidLoad()
progressIncrement = 1.0/duration
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.showProgress), userInfo: nil, repeats: true)
}
@objc func showProgress() { 
if(progressCounter > 1.0){timer.invalidate()}
progressBar.progress = progressCounter
progressCounter = progressCounter + progressIncrement
}
}

Voila! A circular progress Bar is built from scratch. You can download the entire code from the Github link below.