In this article, you will read about how to create the Circular progress view for the iOS apps (Swift) with animation.
Whether you need to show the percentage of the used amount or a fraction of the target number, one of way is to use the progress view. In our case, it will be a simple circular custom view.
In TripExBud we use the circular progress view quite often to represent the percentage form spent amount or available funds within budgets. Enough simple and powerful at the same time the circular view provides attractive visualization of a fraction of the amount or the percentage. Together with rounded corners and animation, it looks awesome.
Now let’s move on to the code
We’re going to programmatically create the custom view and call it from the controller. The circle inside of the view will match the size provided by the controller. Since the circle fits well into the square, so the best is to apply equally width and height.
Create the custom view
First, what we need it’s to create the custom view with initialization methods and the override function “draw”.
import UIKit
class CircularView: UIView{
override init(frame: CGRect){
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func draw(_ rect: CGRect) {
}
}
Our view contains a background view to represent the target. It’s defined as a gray color to highlight the whole circle and to draw frames for future circular view. Moreover, it’s a path for animation as well. The variables “lineWidth” and “lineColor” let’s leave as input parameters to adjust it depends on the screen requirements.
func drawBackRingFittingInsideView(lineWidth: CGFloat, lineColor: UIColor) {
let halfSize:CGFloat = min( bounds.size.width/2, bounds.size.height/2)
let desiredLineWidth:CGFloat = lineWidth
let circle = CGFloat(Double.pi * 2)
let startAngle = CGFloat(circle * 0.1)
let endAngle = circle – startAngle
let circlePath = UIBezierPath(
arcCenter: CGPoint(x:halfSize, y:halfSize),
radius: CGFloat( halfSize – (desiredLineWidth/2) ),
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
let shapeBackLayer = CAShapeLayer()
shapeBackLayer.path = circlePath.cgPath
shapeBackLayer.fillColor = UIColor.clear.cgColor
shapeBackLayer.strokeColor = lineColor.cgColor
shapeBackLayer.lineWidth = desiredLineWidth
shapeBackLayer.lineCap = .round
layer.addSublayer(shapeBackLayer)
}
The circle is not closed, so we left 10% open from the whole circle diameter. We need this place to set up any long number going outside of our circle.
let startAngle = CGFloat(circle * 0.1)
The UIBezierPath is a path that consists of straight and curved line segments that you can render in your custom views. More about UIBezierPath is here..
The CAShapeLayer is a layer that draws a cubic Bezier spline in its coordinate space. More here..
We need to create public variables to adjust them outside of the circular view.
var percent: Double = 0.0
var lineColor: UIColor?
var lineWidth = 3.0
var backgroundLineColor: UIColor?
var backgroundLineWidth = 3.0
let shapeLayer = CAShapeLayer()
The main view looks similar to the background view just with small changes for end angle. It’s a dynamic part and depends on the passed percent.
func drawRingFittingInsideView(lineWidth: CGFloat, lineColor: UIColor, percent: CGFloat) {
let halfSize:CGFloat = min( bounds.size.width/2, bounds.size.height/2)
let desiredLineWidth:CGFloat = lineWidth
let circle = CGFloat(Double.pi * 2)
let startAngle = CGFloat(circle * 0.1)
let maxEndAngle = circle – (startAngle * 2)
let endAngle = ((maxEndAngle * percent) / 100) + startAngle
let circlePath = UIBezierPath(
arcCenter: CGPoint(x:halfSize, y:halfSize),
radius: CGFloat( halfSize – (desiredLineWidth/2) ),
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = lineColor.cgColor
shapeLayer.lineWidth = desiredLineWidth
shapeLayer.lineCap = .round
layer.addSublayer(shapeLayer)
}
Both those methods we add to the “draw” function. In case if the percent is zero we still draw the background view and skip the main view. The math function “min” here to make the upper threshold and not to close the circle.
override func draw(_ rect: CGRect) {
drawBackRingFittingInsideView(lineWidth: CGFloat(backgroundLineWidth),
lineColor: backgroundLineColor ?? .lightGray)
if percent > 0 {
percent = min(100, percent)
drawRingFittingInsideView(lineWidth: CGFloat(lineWidth),
lineColor: lineColor ?? .blue, percent: CGFloat(percent))
}
}
The circular view with animation
The final step in the creation of the circular view is animation. The call and the duration of animation will be determined outside of the view.
func animateCircle(duration: TimeInterval) {
let animation = CABasicAnimation(keyPath: “strokeEnd”)
animation.duration = duration
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
shapeLayer.strokeEnd = 1.0
shapeLayer.add(animation, forKey: “animateCircle”)
}
The CABasicAnimation is an object that provides basic, single-keyframe animation capabilities for a layer property. More here..
Left only build and add programmatically the circular view in the controller view.
let circleView = CircleView(frame: CGRect(x: 0, y: 0, width: 108, height: 108))
circleView.percent = 68.0
circleView.lineColor = .blue
circleView.backgroundLineWidth = 8.0
circleView.lineWidth = 8.0
circleView.animateCircle(duration: 0.8)
self.addSubview(circleView)
The percent or the number inside the circular view can be added and managed as a uilabel outside in the controller view.
How it looks in the app TripExBud follow this link.