Building a custom ring

A pet project of mine requires replicating one of the progress indicators in the Health app. I want to be able to configure the ring’s thickness, colour, and percentage of completion.

Before launching Xcode and start writing code right away, I wrote down what I knew about the problem I wanted to solve:

  • CAShapeLayer seems to be a good fit for this.
  • Animatable properties are, well, animatable. And for free. All you need to do is set their new value, and core animation will take care of animating the transition for you.
  • It is easy to build an oval UIBezierPath.
  • It is easy to apply transformations to bezier paths
  • It should be possible to configure the view both programatically and in Interface Builder.

With that in mind, I decided to build a subclass of UIView, insert a CAShapeLayer into it, and bind the “progress” property of my view to the layer’s strokeEnd property. Also, I decided to took this as an opportunity to explore @IBDesignable and @IBInspectable, which is something I’d never done before.

And this is the end result:


The source code is available on GitHub, but let’s take a look at some of the highlights.

Display a custom view in Interface Builder

This one was easier than I expected. I can not claim to be an expert in Interface Builder, so I expected a lot of friction here, but annotating the class as IBInspectable was enough to make Interface Builder aware of it:

@IBDesignable public class Ring: UIView {

Making the view configurable through interface Builder (as well as programatically) was also quite easy, just by annotating properties as IBInspectable and providing a default value for them:

@IBInspectable public var progress: CGFloat = Constants.defaultProgress {
    didSet {
        progress = normalize(progress)
        pathLayer.strokeEnd = progress

@IBInspectable public var lineWidth: CGFloat = Constants.defaultLineWidth {
    didSet {
        pathLayer.lineWidth = lineWidth

Building the actual ring

There might be a better way to do this -I’m looking at you, UIBezierPath(arcCenter, radius, startAngle, endAngle,clockwise)- but I decided to rely on the fact that UIBezierPath’s strokeEnd is animatable (which means that everytime I cahnge its value, CoreAnimation will animate the changes for me), so I built the path as an oval, and set the strokeEnd to the value of the progress property (which is normalized between 0 and 1).

Of course, that makes the path start at the X axis, so the path’s layer needs to be rotated -PI/2.

After that, it is just a matter of setting the proper frame, and path in the layoutSubviews method. Here is the relevant code:

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


required public init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)


private func configureRing() {
    pathLayer.frame = bounds

    pathLayer.lineWidth = lineWidth
    pathLayer.fillColor = fillColor.CGColor
    pathLayer.strokeColor = lineColor.CGColor


    pathLayer.anchorPoint = CGPointMake(0.5, 0.5)
    pathLayer.transform = CATransform3DRotate(pathLayer.transform, -0.5 * CGFloat(M_PI), 0.0, 0.0, 1.0);

override public func layoutSubviews() {
    pathLayer.frame = bounds
    pathLayer.path = ringPath().CGPath

private func ringPath() -> UIBezierPath {
    return UIBezierPath(ovalInRect: ringFrame())

private func ringFrame() -> CGRect {
    return CGRectInset(CGRect(x: 0, y: 0, width: CGRectGetHeight(bounds), height: CGRectGetHeight(bounds)), lineWidth*2, lineWidth*2)

The full source is available on GitHub, and can be pulled in as a dependency through Carthage.

One Reply to “Building a custom ring”

Leave a Reply

Your email address will not be published. Required fields are marked *