Idea behind this post is described in UIBezierPath, CAAnimation and Swift article.
Requirements
- Indicator should be a subclass of UIView
- Shapes in the indicator have to be like drops
- Movement direction has to be adjustable
- Timing function has to be adjustable
Shapes
In order to create a drop shape, CAShapeLayer is used in a little unusual way.
From the start size of the stroke of the CAShapeLayer to the smallest size there is N-amount of CAShapeLayer instances drawn along a circular UIBezierPath path.
It looks like this when random colours are used for the stroke:

And when filled with the same solid color, it looks like this:

Cut to the Chase
Firstly, shapes need to be placed along UIBezierPath. In order to do this, we divide UIBezierPath’s strokeStart and strokeEnd into chunks according to shape length and shape count:
func setupStartPoints() { for i in 1...self.numberOfShapes { let fraction = (100 / self.numberOfShapes) let pointStart = Float(fraction * i) / 100 self.startPoints.append(pointStart - self.shapeLength * 0.5) } }
It should be noted that when setting a specific self.numberOfShapes, one needs to keep in mind that positioning depends on the count of shapes (i.e. self.numberOfShapes has to be a factor (natural divisor) of 100).
func generateShape(previousShape: CAShapeLayer?, originStart: Float, originEnd: Float) -> CAShapeLayer { let shape = CAShapeLayer() shape.path = self.shapePath.cgPath self.calculateStroke(shape: shape, previousShape: previousShape, originStart: originStart, originEnd: originEnd) shape.lineCap = kCALineCapRound shape.fillColor = UIColor.clear.cgColor shape.strokeColor = self.strokeColor.cgColor return shape }
It is important to set kCALineCapRound in order to achieve smooth curves for the shapes.
Actual calculation is performed in the following method:
func calculateStroke(shape: CAShapeLayer, previousShape: CAShapeLayer?, originStart: Float, originEnd: Float) { let shapeWidthVariance : CGFloat = round((self.shapeOriginWidth * CGFloat(self.shapeLength)) * 10) / 10 var lineWidth: CGFloat = 0 var strokeStart: CGFloat = 0 var strokeEnd: CGFloat = 0 if let previousShape = previousShape { strokeStart = previousShape.strokeStart - CGFloat(self.shapeElementLength) strokeEnd = previousShape.strokeEnd - CGFloat(self.shapeElementLength) lineWidth = previousShape.lineWidth - shapeWidthVariance } else { strokeStart = CGFloat(originStart) strokeEnd = CGFloat(originEnd) lineWidth = self.shapeOriginWidth } shape.lineWidth = lineWidth shape.strokeStart = strokeStart.normalizedShapePoint shape.strokeEnd = strokeEnd.normalizedShapePoint }
So whether we are drawing the first shape, or any other, strokeStart and strokeEnd are based on the self.shapeElementLength, which is 0.01. So in every 0.1 there are 10 shapes with different width values.
As a result, these are progress indicators possible with the provided engine:



Outcome
As a result, we have more-or-less flexible engine with a small customisation options.
Sources can be found here on GitHub.
See you in next animation magic article!
One thought on “Progress Indicator v1”