So I took what Duncan C had in his linked GitHub project (in Objective-C) and applied it to my scenario (also converted it to Swift) - and it works great!
Here's the final solution:
// ---
// In some ViewController
// ---
let animatedCircle = AnimatedCircle()
self.addSubview(animatedCircle)
animatedCircle.runAnimation()
// ---
// The Animated Circle Class
// ---
import UIKit
import Darwin
let pi: CGFloat = CGFloat(M_PI)
let startAngle: CGFloat = (3.0 * pi) / 2.0
let colorGray = UIColor(red: 0.608, green: 0.608, blue: 0.608, alpha: 1.000)
let colorGreen = UIColor(red: 0.482, green: 0.835, blue: 0.000, alpha: 1.000)
// ----
// Math class to handle fun circle forumals
// ----
class Math {
func percentToRadians(percentComplete: CGFloat) -> CGFloat {
let degrees = (percentComplete/100) * 360
return startAngle + (degrees * (pi/180))
}
}
class AnimatedCircle: UIView {
let percentComplete: CGFloat = 25
var containerView: UIView!
var filledLayer: CAShapeLayer!
init() {
super.init(frame: CGRect(x: 0, y: 0, width: 260, height: 260))
let endAngle = Math().percentToRadians(percentComplete)
// ----
// Create oval bezier path and layer
// ----
let ovalPath = UIBezierPath(ovalInRect: CGRectMake(10, 10, 240, 240))
let circleStrokeLayer = CAShapeLayer()
circleStrokeLayer.path = ovalPath.CGPath
circleStrokeLayer.lineWidth = 20
circleStrokeLayer.fillColor = UIColor.clearColor().CGColor
circleStrokeLayer.strokeColor = colorGreen.CGColor
// ----
// Create filled bezier path and layer
// ----
let filledPathStart = UIBezierPath(arcCenter: CGPoint(x: 140, y: 140), radius: 120, startAngle: startAngle, endAngle: endAngle, clockwise: true)
filledPathStart.addLineToPoint(CGPoint(x: 140, y: 140))
filledLayer = CAShapeLayer()
filledLayer.path = filledPathStart.CGPath
filledLayer.fillColor = colorGreen.CGColor
// ----
// Add any layers to container view
// ----
containerView = UIView(frame: self.frame)
containerView.backgroundColor = UIColor.whiteColor()
containerView.layer.addSublayer(circleStrokeLayer)
containerView.layer.addSublayer(filledLayer)
// Set the frame of the filledLayer to match our view
filledLayer.frame = containerView.frame
}
func runAnimation() {
let endAngle = Math().percentToRadians(percentComplete)
let maskLayer = CAShapeLayer()
let maskWidth: CGFloat = filledLayer.frame.size.width
let maskHeight: CGFloat = filledLayer.frame.size.height
let centerPoint: CGPoint = CGPointMake(maskWidth / 2, maskHeight / 2)
let radius: CGFloat = CGFloat(sqrtf(Float(maskWidth * maskWidth + maskHeight * maskHeight)) / 2)
maskLayer.fillColor = UIColor.clearColor().CGColor
maskLayer.strokeColor = UIColor.blackColor().CGColor
maskLayer.lineWidth = radius
let arcPath: CGMutablePathRef = CGPathCreateMutable()
CGPathMoveToPoint(arcPath, nil, centerPoint.x, centerPoint.y - radius / 2)
CGPathAddArc(arcPath, nil, centerPoint.x, centerPoint.y, radius / 2, 3*pi/2, endAngle, false)
maskLayer.path = arcPath
maskLayer.strokeEnd = 0
filledLayer.mask = maskLayer
filledLayer.mask!.frame = filledLayer.frame
let anim = CABasicAnimation(keyPath: "strokeEnd")
anim.duration = 3
anim.delegate = self
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
anim.fillMode = kCAFillModeForwards
anim.removedOnCompletion = false
anim.autoreverses = false
anim.toValue = 1.0
maskLayer.addAnimation(anim, forKey: "strokeEnd")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}