Custom progress indicators in Swift

Custom progress indicators in Swift

One of the most important aspects to take care in mobile development is the feedback to the user, especially in the long running tasks. That implies we need to add some components to give information to the user about what is happening with the app. The most popular forms of such feedback are progress indicators.

If you are developing a super cool app with highly custom user interfaces, UIKit standard indicators won’t likely be enough for your needs. In this post, we’ll develop a fully customizable progress bar, that you can use as a replacement of the UIProgressView.

We start simply adding a new UIView subclass called OBProgressBar

				
					class OBProgressBar: UIView {

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

    required init(coder aDecoder: NSCoder) {
       super.init(coder: aDecoder)!
    }
}
				
			

Suppose we want our progress bar to be a curve instead of the mainstream horizontal line. We need to specify the curve somehow, so we’ll use a specialized subclass of CALayer called CAShapeLayer. This kind of layers are capable of drawing itself using the shape you define via the path property. We can specify it defining a UIBezierPath and retrieve de CGPath from it.

In this case, for the creation of the bezier path, we’ll be using PaintCode, an awesome app that allows you create bezier paths graphically, or create the path from vector images. It is as simple as drawing shapes and curves for exporting it later in Swift code.

Graphic showcasing a process.

After we got the bezier path with a design that we want, we need to add some properties to our class for managing the curves. Let’s add the following at the top of the class:

				
					class OBProgressBar: UIView {
   fileprivate var bezierPath:UIBezierPath!
   fileprivate var progressLayer : CAShapeLayer!
   fileprivate var backgroundLayer : CAShapeLayer!
   . . .
				
			

We’ll use two shapes, one for the bar background, and another for the progress itself. We don’t want these properties to be modified outside the class so we keep it as private. We also need to change the constructor a little bit to let the class know what path we’ll be using. Also, we have to add a method for initializing the shape layers.

				
					init(frame: CGRect, path: UIBezierPath) {
   super.init(frame: frame)
   bezierPath = path
   setup()
}

func setup() {
   progressLayer = CAShapeLayer()
   progressLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
   progressLayer.path = bezierPath.cgPath
   progressLayer.lineWidth = 3
   progressLayer.strokeColor = UIColor.red.cgColor
   progressLayer.fillColor = nil
   progressLayer.strokeEnd = 0.0

   backgroundLayer = CAShapeLayer()
   backgroundLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
   backgroundLayer.path = bezierPath.cgPath
   backgroundLayer.lineWidth = 3
   backgroundLayer.strokeColor = UIColor(colorLiteralRed: 240/255, green: 240/255, blue: 240/255, alpha: 1.0).cgColor
   backgroundLayer.fillColor = nil

   self.layer.addSublayer(backgroundLayer)
   self.layer.addSublayer(progressLayer)
}
				
			

The shape layers are pretty self-describing, you can change its colors, path, line width etc. The key property for our case is the outline: we need to stroke the path to a given percentage according to the actual progress. This is possible with the property strokeEnd, a relative value telling the location to stop stroking. 

Let’s add a property to our class called progress:

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

The didSet observer allows us to do additional tasks when the variable is set. In this case, we need to update the strokeEnd property to match the new progress.

And that’s it, we can use it simply instantiating the class and setting the progress to the according value at a time.

				
					let frame: CGRect = …
let path: UIBezierPath = …
let progressBar = OBProgressBar(frame: frame, path: path)
progressBar.progress = 0.5
				
			

In this example, we attach the value of a slider with our custom bar for seeing it in action. You can use it as a replacement of the UIProgressView, for your downloads progress, long running task or simply to indicate the progress of a video.

Graphic showcasing a process.

Another example

Graphic showcasing a map.

In a future post, we’ll be showing how to implement another key component of mobile user experience, that use pretty much the same code. We are talking about the Activity Indicators. Meanwhile, have fun developing these awesome bars :).

See related posts