UIBezierPath Custom lineCapStyle different on both Ends - ios

What i want to achieve is
What i tried is to draw three arcs with different colors with lineCapStyle .rounded. My code for drawing these arcs are below
private func circularActivityPath(rect:CGRect, configuration:PathConfiguration)-> CGPath {
let center = CGPoint(x: rect.maxX / 2, y: rect.maxY / 2)
let longestSide = rect.height < rect.width ? rect.height : rect.width
let path = UIBezierPath(arcCenter: center, radius: (longestSide / 2) - (configuration.lineWidth / 2), startAngle: configuration.startAngle.deg2rad() , endAngle: configuration.endAngle.deg2rad(), clockwise: true)
path.lineCapStyle = .round
return path.cgPath
}
override func draw(_ rect: CGRect) {
let config1 = PathConfiguration(color: .red, lineWidth: lineWidth, startAngle: CGFloat(-90), endAngle: CGFloat(10), type: .track , shape: shape )
let trackLayer1 = CAShapeLayer()
trackLayer1.drawActivityCircles(in: rect, configuration: config1)
let config2 = PathConfiguration(color: .blue, lineWidth: lineWidth, startAngle: CGFloat(25), endAngle: CGFloat(80), type: .track , shape: shape )
let trackLayer2 = CAShapeLayer()
trackLayer2.drawActivityCircles(in: rect, configuration: config2)
let config3 = PathConfiguration(color: .green, lineWidth: lineWidth, startAngle: CGFloat(95), endAngle: CGFloat(255), type: .track , shape: shape )
let trackLayer3 = CAShapeLayer()
trackLayer3.drawActivityCircles(in: rect, configuration: config3)
self.layer.addSublayer(trackLayer2) // blue
self.layer.addSublayer(trackLayer3) // green
self.layer.addSublayer(trackLayer1) // red
}
where PathConfiguration is a struct
struct PathConfiguration {
let color: UIColor
let lineWidth: CGFloat
let startAngle: CGFloat
let endAngle: CGFloat
let type: TrackType
let shape: TrackShape
}
What i get is below with rounded caps on both sides... i want to achieve one rounded and one arc cap on respective ends. i will be very thankful to you if i get some pointers how i can get the same shape

See the below code to achieve this result.
import UIKit
// MARK: - Enums
public enum AnimationStyle: Int {
case animationFanAll
case animationFan
case animationFadeIn
case animationthreeD
case none
}
public enum PercentageStyle : Int {
case none
case inward
case outward
case over
}
open class Circular: UIView {
// MARK: - Public Properties
public var animationType: AnimationStyle {
get {
return _animationType
}
set(newValue) {
_animationType = newValue
setNeedsDisplay()
}
}
public var showPercentageStyle: PercentageStyle {
get {
return _showPercentageStyle
}
set(newValue) {
_showPercentageStyle = newValue
setNeedsDisplay()
}
}
public var lineWidth: CGFloat {
get {
return _lineWidth
}
set(newValue) {
_lineWidth = newValue
setNeedsDisplay()
}
}
// MARK:- Private Variable
private var _percentages: [Double]
private var _colors: [UIColor]
private var _lineWidth = CGFloat( 10.0)
private var _animationType: AnimationStyle
private var _showPercentageStyle: PercentageStyle
//MARK:- draw
override public func draw(_ rect: CGRect) {
var startAngle = -90.0
for i in 0..<_percentages.count {
let endAngle = startAngle + ( _percentages[i] * 3.6 ) - 4
let shapeLayer = self.addArac(with: _colors[i], in: rect, startAngle: startAngle, endAngle: endAngle)
showAnimationStyle(index: Double(i), shapeLayer: shapeLayer, startAngle: startAngle, endAngle: endAngle)
showPercentages(midAngel:startAngle + (endAngle - startAngle)/2, percentage: _percentages[i])
startAngle = (endAngle + 4 )
}
}
//MARK:- inializer
public init(percentages:[Double],colors:[UIColor],aimationType:AnimationStyle = .animationFanAll , showPercentageStyle: PercentageStyle = .none) {
self._percentages = percentages
self._colors = colors
self._animationType = aimationType
self._showPercentageStyle = showPercentageStyle
super.init(frame:CGRect.zero)
self.backgroundColor = .clear
self.clipsToBounds = false
}
required public init?(coder: NSCoder) {
// super.init(coder: coder)
fatalError("init(coder:) has not been implemented")
}
//MARK:- Animations Functions
private func showAnimationStyle(index:Double,shapeLayer:CAShapeLayer,startAngle:Double,endAngle:Double) {
switch _animationType {
case .animationFanAll:
maskEachLayerAnimation(startAngal: startAngle, endAngal: endAngle + 4 , shape: shapeLayer)
case .animationFan:
if Int(index) == _percentages.count - 1 {
maskAnimation()
}
case .animationFadeIn:
oppacityAnimation(index: index, shape: shapeLayer)
case .animationthreeD:
transformAnimation(index: index, shape: shapeLayer)
case .none:
break
}
}
private func oppacityAnimation(index:Double,shape:CAShapeLayer) {
shape.opacity = 0
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index)/2.5 ) {
shape.opacity = 1
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 1
shape.add(animation, forKey: nil)
}
}
private func transformAnimation(index:Double,shape:CAShapeLayer){
shape.opacity = 0
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index)/2.5 ) {
shape.opacity = 1
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = CATransform3DMakeScale(0, 0, 1)
animation.toValue = CATransform3DIdentity
animation.duration = 1
shape.add(animation, forKey: nil)
}
}
private func maskEachLayerAnimation(startAngal:Double,endAngal:Double,shape:CAShapeLayer){
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = max( bounds.maxX,bounds.maxY)/5
shapeLayer.frame = bounds
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let longestSide = max(bounds.height,bounds.width)
shapeLayer.path = UIBezierPath(arcCenter: center, radius: longestSide/2, startAngle: CGFloat(startAngal).deg2rad(), endAngle: CGFloat(endAngal ).deg2rad(), clockwise: true).cgPath
shapeLayer.strokeEnd = 0
shape.mask = shapeLayer
addAnimationToLayer(toLayer: shape, fromLayer: shapeLayer)
}
private func maskAnimation() {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.lineWidth = max( bounds.maxX,bounds.maxY)/2
shapeLayer.frame = bounds
let path = UIBezierPath(arcCenter: CGPoint(x:bounds.midX,y:bounds.midY), radius:max( bounds.maxX/2,bounds.maxY/2), startAngle: CGFloat(-89.0).deg2rad(), endAngle: CGFloat( 270.0).deg2rad(), clockwise: true)
shapeLayer.path = path.cgPath
shapeLayer.strokeEnd = 0
self.layer.mask = shapeLayer
addAnimationToLayer(toLayer: self.layer, fromLayer: shapeLayer)
}
private func addAnimationToLayer(toLayer:CALayer , fromLayer:CAShapeLayer) {
CATransaction.begin()
CATransaction.setCompletionBlock {
toLayer.mask = nil
}
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.beginTime = CACurrentMediaTime() + 0.3
animation.fromValue = 0
animation.toValue = 1
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
fromLayer.add(animation, forKey: "line")
CATransaction.commit()
}
//MARK:- show percentages
private func showPercentages(midAngel:Double, percentage:Double) {
guard let radius = getRadiusOfPercentage() else {
return
}
let center = CGPoint(x: bounds.maxX / 2, y: bounds.maxY / 2)
let x = center.x + (radius) * CGFloat(cos(CGFloat(midAngel).deg2rad()))
let y = center.y + (radius) * CGFloat(sin(CGFloat(midAngel).deg2rad()))
let percentageLabel = UILabel(frame: CGRect.zero)
percentageLabel.frame = CGRect.zero
percentageLabel.text = String(percentage)
percentageLabel.textColor = .black
percentageLabel.font = UIFont.boldSystemFont(ofSize: 12)
percentageLabel.sizeToFit()
percentageLabel.center = CGPoint(x:x,y:y)
addSubview(percentageLabel)
percentageLabel.alpha = 0
var delay = 1.5
if self.animationType == .none {
delay = 0
}
UIView.animate(withDuration: 0.5, delay: delay, options: .curveEaseOut, animations: {
percentageLabel.alpha = 1
})
}
private func getRadiusOfPercentage() -> CGFloat? {
let longestSide = max(bounds.height,bounds.width)
switch self.showPercentageStyle {
case .inward:
return longestSide/3 - lineWidth
case .over:
return longestSide/2 - lineWidth
case .outward:
return longestSide/2 + lineWidth + 5
case .none:
return nil
}
}
//MARK:- Drawing Code
private func addArac(with color:UIColor ,in rect:CGRect, startAngle:Double , endAngle:Double)-> CAShapeLayer {
let center = CGPoint(x: rect.maxX / 2, y: rect.maxY / 2)
let longestSide = max(rect.height,rect.width)
let lineWidth = CGFloat(self._lineWidth / 20)
let smallCircleRadious = (longestSide / (2 + lineWidth))
let startAngle = CGFloat(startAngle)
let endAngle = CGFloat(endAngle)
let outerRadious = (longestSide / 2)
let midPoint = (longestSide / (2 + lineWidth/2.7))
let path = UIBezierPath()
let x3 = center.x + (outerRadious) * CGFloat(cos(startAngle.deg2rad()))
let y3 = center.y + (outerRadious) * CGFloat(sin(startAngle.deg2rad()))
let x4 = center.x + (smallCircleRadious) * CGFloat(cos(startAngle.deg2rad()))
let y4 = center.y + (smallCircleRadious) * CGFloat(sin(startAngle.deg2rad()))
let x5 = center.x + (midPoint) * CGFloat(cos((startAngle + self._lineWidth * 0.5).deg2rad()))
let y5 = center.y + (midPoint) * CGFloat(sin((startAngle + self._lineWidth * 0.5).deg2rad()))
path.move(to: CGPoint(x:x4,y:y4))
path.addQuadCurve(to: CGPoint(x:x3,y:y3), controlPoint: CGPoint(x:x5,y:y5))
path.addArc(withCenter:center, radius:outerRadious, startAngle: startAngle.deg2rad() , endAngle: endAngle.deg2rad(), clockwise: true)
let x1 = center.x + (outerRadious) * CGFloat(cos(endAngle.deg2rad()))
let y1 = center.y + (outerRadious) * CGFloat(sin(endAngle.deg2rad()))
let x6 = center.x + (midPoint) * CGFloat(cos((endAngle + self._lineWidth * 0.6).deg2rad()))
let y6 = center.y + (midPoint) * CGFloat(sin((endAngle + self._lineWidth * 0.6).deg2rad()))
let x2 = center.x + (smallCircleRadious) * CGFloat(cos(endAngle.deg2rad()))
let y2 = center.y + (smallCircleRadious) * CGFloat(sin(endAngle.deg2rad()))
path.move(to: CGPoint(x:x1,y:y1))
path.addQuadCurve(to: CGPoint(x:x2,y:y2), controlPoint: CGPoint(x:x6,y:y6))
path.addArc(withCenter:center, radius: smallCircleRadious, startAngle: endAngle.deg2rad(), endAngle: startAngle.deg2rad(), clockwise: false)
let shape = CAShapeLayer()
shape.frame = bounds
shape.lineCap = .round
shape.fillColor = color.cgColor
shape.path = path.cgPath
layer.addSublayer(shape)
return shape
}
}
extension CGFloat {
func deg2rad() -> CGFloat {
return self * .pi / 180
}
}

Related

How to create Circle with step progress (gaps in it) and animate it?

I need to create a progressive with gaps in it and Animate the layers. I have achieved it. But the problem is it is starting (0) from Right centre. But the requirement is it should start from top centre. In image You can see that it is started from right side.
I have attached my code sample along with Image for your understanding. Can somebody help me where I'm doing wrong or how should I make it from top.
extension ViewController {
func sampleProgress() {
let totalSteps = 6
let frame = CGRect(x: 50, y: 50, width: 120, height: 120)
let circlePath = UIBezierPath(ovalIn: frame)
let gapSize: CGFloat = 0.0125
let segmentAngle: CGFloat = 0.167 // (1/totalSteps)
var startAngle = 0.0
let lineWidth = 8.0
for index in 0 ... totalSteps {
// Background layer
let backgroundLayer = CAShapeLayer()
backgroundLayer.strokeStart = startAngle
backgroundLayer.strokeEnd = backgroundLayer.strokeStart + segmentAngle - gapSize
backgroundLayer.path = circlePath.cgPath
backgroundLayer.name = String(index)
backgroundLayer.strokeColor = UIColor.lightGray.cgColor
backgroundLayer.lineWidth = lineWidth
backgroundLayer.lineCap = CAShapeLayerLineCap.butt
backgroundLayer.fillColor = UIColor.clear.cgColor
self.view.layer.addSublayer(backgroundLayer)
// Foreground layer
let foregroundLayer = CAShapeLayer()
foregroundLayer.strokeStart = startAngle
foregroundLayer.strokeEnd = backgroundLayer.strokeStart + segmentAngle - gapSize
foregroundLayer.isHidden = true
foregroundLayer.name = String(index) + String(index)
foregroundLayer.path = circlePath.cgPath
foregroundLayer.strokeColor = UIColor.green.cgColor
foregroundLayer.lineWidth = lineWidth
foregroundLayer.lineCap = CAShapeLayerLineCap.butt
foregroundLayer.fillColor = UIColor.clear.cgColor
self.view.layer.addSublayer(foregroundLayer)
print("Start angle: \(startAngle)")
startAngle = startAngle + segmentAngle
}
}
func animateLayer(isAnimate: Bool, stepsToAnimate: Int) {
let segmentAngle: CGFloat = (360 * 0.166) / 360
let gapSize: CGFloat = 0.0125
var startAngle = 0.0
for index in 0 ... stepsToAnimate {
if let foregroundLayers = self.view.layer.sublayers {
for animateLayer in foregroundLayers {
if animateLayer.name == String(index) + String(index) {
if index == stepsToAnimate && isAnimate {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = startAngle
animation.toValue = startAngle + segmentAngle - gapSize
animation.duration = 1.0
animateLayer.add(animation, forKey: "foregroundAnimation")
animateLayer.isHidden = false
} else {
animateLayer.isHidden = false
}
startAngle = startAngle + segmentAngle
}
}
}
}
}
}
You can "move the start" to the top by rotating the layer(s) minus 90-degrees:
let tr = CATransform3DMakeRotation(-(.pi * 0.5), 0, 0, 1)
I would assume this would be wrapped into a UIView subclass, but to get your example (adding sublayers to the main view's layer) to work right, we'll want to use a Zero-based origin for the path rect:
// use 0,0 for the origin of the PATH frame
let frame = CGRect(x: 0, y: 0, width: 120, height: 120)
let circlePath = UIBezierPath(ovalIn: frame)
and then an offset rect for the position:
let layerFrame = frame.offsetBy(dx: 50, dy: 50)
and we set the .anchorPoint of the layers to the center of that rect -- so it will rotate around its center:
// set the layer's frame
backgroundLayer.frame = layerFrame
// set the layer's anchor point
backgroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
backgroundLayer.transform = tr
// set the layer's frame
foregroundLayer.frame = layerFrame
// set the layer's anchor point
foregroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
foregroundLayer.transform = tr
So, slight modifications to your code:
extension ViewController {
func sampleProgress() {
let totalSteps = 6
// use 0,0 for the origin of the PATH frame
let frame = CGRect(x: 0, y: 0, width: 120, height: 120)
let circlePath = UIBezierPath(ovalIn: frame)
// use this for the POSITION of the path
let layerFrame = frame.offsetBy(dx: 50, dy: 50)
let gapSize: CGFloat = 0.0125
let segmentAngle: CGFloat = 0.167 // (1/totalSteps)
var startAngle = 0.0
let lineWidth = 8.0
// we want to rotate the layer by -90 degrees
let tr = CATransform3DMakeRotation(-(.pi * 0.5), 0, 0, 1)
for index in 0 ... totalSteps {
// Background layer
let backgroundLayer = CAShapeLayer()
backgroundLayer.strokeStart = startAngle
backgroundLayer.strokeEnd = backgroundLayer.strokeStart + segmentAngle - gapSize
backgroundLayer.path = circlePath.cgPath
backgroundLayer.name = String(index)
backgroundLayer.strokeColor = UIColor.lightGray.cgColor
backgroundLayer.lineWidth = lineWidth
backgroundLayer.lineCap = CAShapeLayerLineCap.butt
backgroundLayer.fillColor = UIColor.clear.cgColor
self.view.layer.addSublayer(backgroundLayer)
// set the layer's frame
backgroundLayer.frame = layerFrame
// set the layer's anchor point
backgroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
backgroundLayer.transform = tr
// Foreground layer
let foregroundLayer = CAShapeLayer()
foregroundLayer.strokeStart = startAngle
foregroundLayer.strokeEnd = backgroundLayer.strokeStart + segmentAngle - gapSize
foregroundLayer.isHidden = true
foregroundLayer.name = String(index) + String(index)
foregroundLayer.path = circlePath.cgPath
foregroundLayer.strokeColor = UIColor.green.cgColor
foregroundLayer.lineWidth = lineWidth
foregroundLayer.lineCap = CAShapeLayerLineCap.butt
foregroundLayer.fillColor = UIColor.clear.cgColor
self.view.layer.addSublayer(foregroundLayer)
// set the layer's frame
foregroundLayer.frame = layerFrame
// set the layer's anchor point
foregroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
foregroundLayer.transform = tr
print("Start angle: \(startAngle)")
startAngle = startAngle + segmentAngle
}
}
func animateLayer(isAnimate: Bool, stepsToAnimate: Int) {
let segmentAngle: CGFloat = (360 * 0.166) / 360
let gapSize: CGFloat = 0.0125
var startAngle = 0.0
for index in 0 ... stepsToAnimate {
if let foregroundLayers = self.view.layer.sublayers {
for animateLayer in foregroundLayers {
if animateLayer.name == String(index) + String(index) {
if index == stepsToAnimate && isAnimate {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = startAngle
animation.toValue = startAngle + segmentAngle - gapSize
animation.duration = 1.0
animateLayer.add(animation, forKey: "foregroundAnimation")
animateLayer.isHidden = false
} else {
animateLayer.isHidden = false
}
startAngle = startAngle + segmentAngle
}
}
}
}
}
}
and an example controller - each tap anywhere animates the next step:
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
sampleProgress()
}
var p: Int = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
animateLayer(isAnimate: true, stepsToAnimate: p)
p += 1
}
}

How to do hit detection in core graphics

Core graphics is pretty new to me, and I'm facing some issues detecting clicks on my custom graphics.
I generated som code with the demo of paincode which i then heavily modified. It draws a "pie" like this:
The code I used for this looks like this:
import UIKit
public class DrawTest : NSObject {
static var hitAreas = [Int:UIBezierPath]()
static func didHit(_ point: CGPoint){
let res = hitAreas.first{ $0.value.contains(point) }?.key
print("HIT: ", res)
}
public class func drawDartboard(frame targetFrame: CGRect) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let resizedFrame: CGRect = targetFrame
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100)
let sliceRect = CGRect(x: 0, y: 0, width: 100, height: 100)
context.saveGState()
context.clip(to: sliceRect)
context.translateBy(x: sliceRect.minX, y: sliceRect.minY)
context.translateBy(x: 0, y: sliceRect.height)
context.scaleBy(x: 1, y: -1)
let dark = UIColor(red: 0.235, green: 0.208, blue: 0.208, alpha: 1.000)
let light = UIColor(red: 0.435, green: 0.408, blue: 0.408, alpha: 1.000)
var slice = 0
while slice < 20 {
let sliceColor = slice%2 == 0 ? dark : light
DrawTest.drawSlice(frame: CGRect(origin: .zero, size: sliceRect.size), roration: CGFloat(slice*18), sliceColor: sliceColor, slice: slice )
slice += 1
}
context.restoreGState()
}
public class func drawSlice(frame targetFrame: CGRect, roration: CGFloat, sliceColor: UIColor, slice: Int) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let resizedFrame: CGRect = targetFrame
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100)
context.saveGState()
context.translateBy(x: 49.99, y: 50)
context.rotate(by: roration * CGFloat.pi/180)
let sliceFillPath = UIBezierPath()
sliceFillPath.move(to: CGPoint(x: -7.82, y: 49.38))
sliceFillPath.addCurve(to: CGPoint(x: 7.83, y: 49.38), controlPoint1: CGPoint(x: -2.63, y: 50.2), controlPoint2: CGPoint(x: 2.65, y: 50.2))
sliceFillPath.addLine(to: CGPoint(x: 0.01, y: -0))
sliceFillPath.addLine(to: CGPoint(x: -7.82, y: 49.38))
sliceFillPath.close()
sliceColor.setFill()
sliceFillPath.fill()
hitAreas[slice] = sliceFillPath
context.restoreGState()
}
}
I'm calling the draw code from a simple UIView subclass like below. This is also were I attach a TapGerstureRecognizer.
import UIKit
class DartBoardView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
#objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
DrawTest.didHit(loc)
}
}
override func draw(_ rect: CGRect) {
DrawTest.drawDartboard(frame: bounds)
}
}
The drawing looks like I want it to, but I want to be able to select each of the slices, this is the part that is not working. I am pretty sure that the issue has to do with the point I pass to didHit is local to my View but the UIBezierPath I store in hitAreas and call contains uses the local coordinates of the UIBezierPath, this is why I never get a hit.
I have no idea how to solve this and desperately need help. My guess is that this should be solved by 1) drawing my slices directy on the UIView´s coordinate system, but that would require a lot af math 2) somehow translate the local coordinates of each UIBezierPath to the scope of the view when hit testing
This is all very confusing at all constructive input is very appreciated.
There are various approaches, depending on exactly what your end-goal is.
One approach:
calculate the "degrees-per-slice" ... 360 / 20 = 18
get the angle from the center point to the touch point
"fix" the angle by 1/2 of the slice width (since the slices don't start at zero)
divide that angle by degrees-per-slice to get the slice number
Use these two extensions to make it easy to get the angle (in degrees):
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
extension CGPoint {
func angle(to otherPoint: CGPoint) -> CGFloat {
let pX = otherPoint.x - x
let pY = otherPoint.y - y
let radians = atan2f(Float(pY), Float(pX))
var degrees = CGFloat(radians).degrees
while degrees < 0 {
degrees += 360
}
return degrees
}
}
And, in the code you posted, in your DrawTest class, change didHit to:
static func didHit(_ point: CGPoint, in bounds: CGRect){
let c: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let angle = c.angle(to: point)
var fixedAngle = Int(angle) + 99 // 90 degrees + 1/2 of slice width
if fixedAngle >= 360 {
fixedAngle -= 360
}
print("HIT:", fixedAngle / 18)
}
and include the bounds when you call it from DartBoardView class as:
#objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
// include self's bounds
DrawTest.didHit(loc, in: bounds)
}
}
Drawbacks include:
you'd also need to check the "line length" to make sure it doesn't extend outside the circle
you don't have easy access to the slice bezier paths (if you want to do something else with them)
Another approach would be to use shape layers for each slice, making it easier to track the bezier paths.
Start with a Struct for the slices:
struct Slice {
var color: UIColor = .white
var path: UIBezierPath = UIBezierPath()
var shapeLayer: CAShapeLayer = CAShapeLayer()
var key: Int = 0
}
The DartBoardView class becomes (note: it uses the same CGFloat extension from above):
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
class DartBoardView: UIView {
// array of slices
var slices: [Slice] = []
// slice width in degrees
let sliceWidth: CGFloat = 360.0 / 20.0
// easy to understand 12 o'clock (3 o'clock is Zero)
let twelveOClock: CGFloat = 270
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let dark = UIColor(red: 0.235, green: 0.208, blue: 0.208, alpha: 1.000)
let light = UIColor(red: 0.435, green: 0.408, blue: 0.408, alpha: 1.000)
for slice in 0..<20 {
let sliceColor = slice % 2 == 1 ? dark : light
let s = Slice(color: sliceColor, key: slice)
s.shapeLayer.fillColor = s.color.cgColor
layer.addSublayer(s.shapeLayer)
slices.append(s)
}
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
#objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
if let s = slices.first(where: { $0.path.contains(loc) }) {
print("HIT:", s.key)
} else {
print("Tapped outside the circle!")
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
let c: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let radius: CGFloat = bounds.midX
// slice width in radians
let ww: CGFloat = sliceWidth.radians
// start 1/2 sliceWidth less than 12 o'clock
var startDegrees: CGFloat = twelveOClock.radians - (ww * 0.5)
for i in 0..<slices.count {
let endDegrees: CGFloat = startDegrees + ww
let pth: UIBezierPath = UIBezierPath()
pth.addArc(withCenter: c, radius: radius, startAngle: startDegrees, endAngle: endDegrees, clockwise: true)
pth.addLine(to: c)
pth.close()
slices[i].path = pth
slices[i].shapeLayer.path = pth.cgPath
startDegrees = endDegrees
}
}
}
And here's an example controller class to demonstrate:
class DartBoardViewController: UIViewController {
let dartBoard = DartBoardView()
override func viewDidLoad() {
super.viewDidLoad()
dartBoard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dartBoard)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
dartBoard.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
dartBoard.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
dartBoard.heightAnchor.constraint(equalTo: dartBoard.widthAnchor),
dartBoard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
dartBoard.backgroundColor = .black
}
}
Edit
Not as complex as it may seem.
Here's an implementation of a full Dart Board (without the numbers - I'll leave that as an exercise for you):
Segment Struct
struct Segment {
var value: Int = 0
var multiplier: Int = 1
var color: UIColor = .cyan
var path: UIBezierPath = UIBezierPath()
var layer: CAShapeLayer = CAShapeLayer()
}
DartBoardView class
class DartBoardView: UIView {
var doubleSegments: [Segment] = [Segment]()
var outerSingleSegments: [Segment] = [Segment]()
var tripleSegments: [Segment] = [Segment]()
var innerSingleSegments: [Segment] = [Segment]()
var singleBullSegment: Segment = Segment()
var doubleBullSegment: Segment = Segment()
var allSegments: [Segment] = [Segment]()
let boardLayer: CAShapeLayer = CAShapeLayer()
let darkColor: UIColor = UIColor(white: 0.1, alpha: 1.0)
let lightColor: UIColor = UIColor(red: 0.975, green: 0.9, blue: 0.8, alpha: 1.0)
let darkRedColor: UIColor = UIColor(red: 0.8, green: 0.1, blue: 0.1, alpha: 1.0)
let darkGreenColor: UIColor = UIColor(red: 0.0, green: 0.5, blue: 0.3, alpha: 1.0)
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(boardLayer)
boardLayer.fillColor = UIColor.black.cgColor
// points starting at 3 o'clock
let values: [Int] = [
6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5, 20, 1, 18, 4, 13,
]
// local vars for reuse
var seg: Segment = Segment()
var c: UIColor = .white
// doubles and triples
for i in 0..<values.count {
c = i % 2 == 1 ? darkRedColor : darkGreenColor
seg = Segment(value: values[i],
multiplier: 2,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
doubleSegments.append(seg)
seg = Segment(value: values[i],
multiplier: 3,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
tripleSegments.append(seg)
}
// singles
for i in 0..<values.count {
c = i % 2 == 1 ? darkColor : lightColor
seg = Segment(value: values[i],
multiplier: 1,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
outerSingleSegments.append(seg)
seg = Segment(value: values[i],
multiplier: 1,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
innerSingleSegments.append(seg)
}
// bull and double bull
seg = Segment(value: 25,
multiplier: 1,
color: darkGreenColor,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
singleBullSegment = seg
seg = Segment(value: 25,
multiplier: 2,
color: darkRedColor,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
doubleBullSegment = seg
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
#objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
if let s = allSegments.first(where: { $0.path.contains(loc) }) {
print("HIT:", s.multiplier == 3 ? "Triple" : s.multiplier == 2 ? "Double" : "Single", s.value)
} else {
print("Tapped outside!")
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
// initialize local variables for reuse / readability
var startAngle: CGFloat = 0
var outerDoubleRadius: CGFloat = 0.0
var innerDoubleRadius: CGFloat = 0.0
var outerTripleRadius: CGFloat = 0.0
var innerTripleRadius: CGFloat = 0.0
var outerBullRadius: CGFloat = 0.0
var innerBullRadius: CGFloat = 0.0
// initialize local constants
let viewCenter: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
// leave 20% for the numbers area
let diameter = bounds.width * 0.8
// dart board radii in mm
let specRadii: [CGFloat] = [
170, 162, 107, 99, 16, 6
]
// convert to view size
let factor: CGFloat = (diameter * 0.5) / specRadii[0]
outerDoubleRadius = specRadii[0] * factor
innerDoubleRadius = specRadii[1] * factor
outerTripleRadius = specRadii[2] * factor
innerTripleRadius = specRadii[3] * factor
outerBullRadius = specRadii[4] * factor
innerBullRadius = specRadii[5] * factor
let wireColor: UIColor = UIColor(white: 0.8, alpha: 1.0)
let wedgeWidth: CGFloat = 360.0 / 20.0
let incAngle: CGFloat = wedgeWidth.radians
startAngle = -(incAngle * 0.5)
var path: UIBezierPath = UIBezierPath()
// outer board layer
path = UIBezierPath(ovalIn: bounds)
boardLayer.path = path.cgPath
for i in 0..<20 {
let endAngle = startAngle + incAngle
var shape = doubleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerDoubleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: innerDoubleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
doubleSegments[i].path = path
shape.fillColor = doubleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = outerSingleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: innerDoubleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: outerTripleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
outerSingleSegments[i].path = path
shape.fillColor = outerSingleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = tripleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerTripleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: innerTripleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
tripleSegments[i].path = path
shape.fillColor = tripleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = innerSingleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: innerTripleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: outerBullRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
innerSingleSegments[i].path = path
shape.fillColor = innerSingleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
startAngle = endAngle
}
let singleBullPath = UIBezierPath(ovalIn: CGRect(x: viewCenter.x - outerBullRadius, y: viewCenter.y - outerBullRadius, width: outerBullRadius * 2, height: outerBullRadius * 2))
let doubleBullPath = UIBezierPath(ovalIn: CGRect(x: viewCenter.x - innerBullRadius, y: viewCenter.y - innerBullRadius, width: innerBullRadius * 2, height: innerBullRadius * 2))
var shape = singleBullSegment.layer
singleBullPath.append(doubleBullPath)
singleBullPath.usesEvenOddFillRule = true
shape.fillRule = .evenOdd
shape.path = singleBullPath.cgPath
singleBullSegment.path = singleBullPath
shape.fillColor = singleBullSegment.color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = doubleBullSegment.layer
shape.path = doubleBullPath.cgPath
doubleBullSegment.path = doubleBullPath
shape.fillColor = doubleBullSegment.color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
// append all segments for hit-testing
allSegments = []
allSegments.append(contentsOf: tripleSegments)
allSegments.append(contentsOf: outerSingleSegments)
allSegments.append(contentsOf: doubleSegments)
allSegments.append(contentsOf: innerSingleSegments)
allSegments.append(singleBullSegment)
allSegments.append(doubleBullSegment)
}
}
CGFloat extension
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
Example view controller
class DartBoardViewController: UIViewController {
let dartBoard = DartBoardView()
override func viewDidLoad() {
super.viewDidLoad()
dartBoard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dartBoard)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
dartBoard.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
dartBoard.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
dartBoard.heightAnchor.constraint(equalTo: dartBoard.widthAnchor),
dartBoard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
dartBoard.backgroundColor = .clear
}
}
Result:
and debug output from a few taps:
HIT: Double 20
HIT: Single 18
HIT: Triple 2
HIT: Single 25
HIT: Double 25

How do I get the coordinates from CAShapeLayer

So I am trying to make a progress bar. So I have made circular path, but I want the dot to be at the end of the progress bar, but how do I get the position of the dot to be att the end of the current progress?
private func simpleShape() {
let width: CGFloat = 10
createCircle()
//make circle transparant in middle
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = UIColor.blue.cgColor
progressLayer.lineCap = CAShapeLayerLineCap.round
progressLayer.lineWidth = width
progressLayer.strokeStart = 0
progressLayer.strokeEnd = 0
//unfilled
backLayer.lineWidth = width
backLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1).cgColor
backLayer.strokeEnd = 1
self.layer.addSublayer(gradientLayer)
}
private func createCircle() {
//create circle
let circle = UIView(frame: bounds)
circle.layoutIfNeeded()
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius: CGFloat = circle.bounds.width / 2 * 0.83
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.475 * Double.pi), endAngle: CGFloat(1.525 * Double.pi), clockwise: true)
//add layers
progressLayer.path = circlePath.cgPath
backLayer.path = circlePath.cgPath
circle.layer.addSublayer(backLayer)
circle.layer.addSublayer(progressLayer)
addSubview(circle)
circle.layer.addSublayer(dotLayer)
}
let dotLayer = CAShapeLayer()
public func setProgress(_ progress: CGFloat) {
progressLayer.strokeEnd = CGFloat(progress)
if let progressEndpoint = progressLayer.path?.currentPoint {
dotLayer.position = progressEndpoint
}
}
This is what I'm getting
This is what I want
You’re going to have to calculate it yourself. So figure out the angle from the start and end angles for your arcs:
let angle = (endAngle - startAngle) * progress + startAngle
And then use basic trigonometry to determine where that point falls:
let point = CGPoint(x: centerPoint.x + radius * cos(angle),
y: centerPoint.y + radius * sin(angle))
dotLayer.position = point
By the way, I’d suggest separating the adding of the sublayers (which is part of the initial configuration process) from the updating paths (which is part of the view layout process, which may be called again if the frame of the view changes, constraints are applied, etc). Thus, perhaps:
#IBDesignable
class ProgressView: UIView {
var progress: CGFloat = 0 { didSet { updateProgress() } }
private var centerPoint: CGPoint = .zero
private var radius: CGFloat = 0
private let startAngle: CGFloat = -0.475 * .pi
private let endAngle: CGFloat = 1.525 * .pi
private let lineWidth: CGFloat = 10
private lazy var progressLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineCap = .round
shapeLayer.lineWidth = lineWidth
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = progress
return shapeLayer
}()
private lazy var backLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = lineWidth
shapeLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1).cgColor
return shapeLayer
}()
private lazy var dotLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.path = UIBezierPath(arcCenter: .zero, radius: lineWidth / 2 * 1.75, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
shapeLayer.fillColor = UIColor.white.withAlphaComponent(0.5).cgColor
return shapeLayer
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSublayers()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addSublayers()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePaths()
updateProgress()
}
}
private extension ProgressView {
func addSublayers() {
layer.addSublayer(backLayer)
layer.addSublayer(progressLayer)
layer.addSublayer(dotLayer)
}
func updatePaths() {
centerPoint = CGPoint(x: bounds.midX, y: bounds.midY)
radius = min(bounds.width, bounds.height) / 2 * 0.83
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
progressLayer.path = circlePath.cgPath
backLayer.path = circlePath.cgPath
}
func updateProgress() {
progressLayer.strokeEnd = progress
let angle = (endAngle - startAngle) * progress + startAngle
let point = CGPoint(x: centerPoint.x + radius * cos(angle),
y: centerPoint.y + radius * sin(angle))
dotLayer.position = point
}
}
What you need is rotation Animation
let progressLayer = CAShapeLayer()
let backLayer = CAShapeLayer()
private func simpleShape() {
let width: CGFloat = 15
createCircle()
//make circle transparant in middle
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = #colorLiteral(red: 0.888897419, green: 0.5411034822, blue: 0.04008810222, alpha: 1)
progressLayer.lineCap = CAShapeLayerLineCap.round
progressLayer.lineWidth = width
progressLayer.strokeStart = 0
progressLayer.strokeEnd = 0
//unfilled
backLayer.lineWidth = width
backLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1)
backLayer.strokeEnd = 1
// self.layer.addSublayer(gradientLayer)
}
private func createCircle() {
//create circle
let circle = UIView(frame: bounds)
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius: CGFloat = circle.bounds.width / 2 * 0.83
let distance = circle.bounds.width / 2 * 0.17
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.475 * Double.pi), endAngle: CGFloat(1.525 * Double.pi), clockwise: true)
//add layers
progressLayer.path = circlePath.cgPath
backLayer.path = circlePath.cgPath
circle.layer.addSublayer(backLayer)
circle.layer.addSublayer(progressLayer)
addSubview(circle)
let circleCenter = CGPoint(x:centerPoint.x - distance,y:centerPoint.y - circleRadius )
let dotCircle = UIBezierPath()
dotCircle.addArc(withCenter:circleCenter, radius: 3, startAngle: CGFloat(-90).deg2rad(), endAngle: CGFloat(270).deg2rad(), clockwise: true)
dotLayer.path = dotCircle.cgPath
dotLayer.position = CGPoint(x:centerPoint.x,y:centerPoint.y )
dotLayer.strokeColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.6496753961)
dotLayer.lineWidth = 10
dotLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
dotLayer.isHidden = true
circle.layer.addSublayer(dotLayer)
}
let dotLayer = CAShapeLayer()
public func setProgress(_ progress: CGFloat) {
print(progress)
// progressLayer.strokeEnd = progress
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.beginTime = CACurrentMediaTime() + 0.5;
animation.fromValue = 0
animation.toValue = progress
animation.duration = 2
animation.autoreverses = false
animation.repeatCount = .nan
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
progressLayer.add(animation, forKey: "line")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.dotLayer.isHidden = false
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
// rotateAnimation.beginTime = CACurrentMediaTime() + 0.5;
rotateAnimation.fromValue = (CGFloat( -90)).deg2rad()
rotateAnimation.toValue = (360*progress - 98).deg2rad()
rotateAnimation.duration = 2
rotateAnimation.fillMode = .forwards
rotateAnimation.isRemovedOnCompletion = false
self.dotLayer.add(rotateAnimation, forKey: nil)
}
}

Animate Custom UIView Property in Swift

all!
I have created a circular progress view using CoreGraphics that looks and updates like so:
50%
75%
The class is a UIView class, and it has a variable called 'progress' that determines how much of the circle is filled in.
It works well, but I want to be able to animate changes to the progress variable so that the bar animates smoothly.
I have read from myriad examples that I need to have a CALayer class along with the View class, which I have made, however, it doesn't animate at all.
Two questions:
Can I keep the graphic I drew in CoreGraphics, or do I need to somehow redraw it in CALayer?
My current (attempted) solution crashes towards the bottom at: anim.fromValue = pres.progress. What's up?
class CircleProgressView: UIView {
#IBInspectable var backFillColor: UIColor = UIColor.blueColor()
#IBInspectable var fillColor: UIColor = UIColor.greenColor()
#IBInspectable var strokeColor: UIColor = UIColor.greenColor()
dynamic var progress: CGFloat = 0.00 {
didSet {
self.layer.setValue(progress, forKey: "progress")
}
}
var distToDestination: CGFloat = 10.0
#IBInspectable var arcWidth: CGFloat = 20
#IBInspectable var outlineWidth: CGFloat = 5
override class func layerClass() -> AnyClass {
return CircleProgressLayer.self
}
override func drawRect(rect: CGRect) {
var fillColor = self.fillColor
if distToDestination < 3.0 {
fillColor = UIColor.greenColor()
} else {
fillColor = self.fillColor
}
//Drawing the inside of the container
//Drawing in the container
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let radius: CGFloat = max(bounds.width, bounds.height) - 10
let startAngle: CGFloat = 3 * π / 2
let endAngle: CGFloat = 3 * π / 2 + 2 * π
let path = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = arcWidth
backFillColor.setStroke()
path.stroke()
let fill = UIColor.blueColor().colorWithAlphaComponent(0.15)
fill.setFill()
path.fill()
//Drawing the fill path. Same process
let fillAngleLength = (π) * progress
let fillStartAngle = 3 * π / 2 - fillAngleLength
let fillEndAngle = 3 * π / 2 + fillAngleLength
let fillPath_fill = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: fillStartAngle, endAngle: fillEndAngle, clockwise: true)
fillPath_fill.lineWidth = arcWidth
fillColor.setStroke()
fillPath_fill.stroke()
//Drawing container outline on top
let outlinePath_outer = UIBezierPath(arcCenter: center, radius: radius / 2 - outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let outlinePath_inner = UIBezierPath(arcCenter: center, radius: radius / 2 - arcWidth + outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
outlinePath_outer.lineWidth = outlineWidth
outlinePath_inner.lineWidth = outlineWidth
strokeColor.setStroke()
outlinePath_outer.stroke()
outlinePath_inner.stroke()
}
}
class CircleProgressLayer: CALayer {
#NSManaged var progress: CGFloat
override class func needsDisplayForKey(key: String) -> Bool {
if key == "progress" {
return true
}
return super.needsDisplayForKey(key)
}
override func actionForKey(key: String) -> CAAction? {
if (key == "progress") {
if let pres = self.presentationLayer() {
let anim: CABasicAnimation = CABasicAnimation.init(keyPath: key)
anim.fromValue = pres.progress
anim.duration = 0.2
return anim
}
return super.actionForKey(key)
} else {
return super.actionForKey(key)
}
}
}
Thanks for the help!
Try this out :)
class ViewController: UIViewController {
let progressView = CircleProgressView(frame:CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200))
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.frame = CGRectMake(0, 300, 200, 100)
button.backgroundColor = UIColor.yellowColor()
button.addTarget(self, action: #selector(ViewController.tap), forControlEvents: UIControlEvents.TouchUpInside)
view.addSubview(button)
view.addSubview(progressView)
progressView.progress = 1.0
}
func tap() {
if progressView.progress == 0.5 {
progressView.progress = 1.0
} else {
progressView.progress = 0.5
}
}
}
class CircleProgressView: UIView {
dynamic var progress: CGFloat = 0.00 {
didSet {
let animation = CABasicAnimation()
animation.keyPath = "progress"
animation.fromValue = circleLayer().progress
animation.toValue = progress
animation.duration = Double(0.5)
self.layer.addAnimation(animation, forKey: "progress")
circleLayer().progress = progress
}
}
func circleLayer() -> CircleProgressLayer {
return self.layer as! CircleProgressLayer
}
override class func layerClass() -> AnyClass {
return CircleProgressLayer.self
}
}
class CircleProgressLayer: CALayer {
#NSManaged var progress: CGFloat
override class func needsDisplayForKey(key: String) -> Bool {
if key == "progress" {
return true
}
return super.needsDisplayForKey(key)
}
var backFillColor: UIColor = UIColor.blueColor()
var fillColor: UIColor = UIColor.greenColor()
var strokeColor: UIColor = UIColor.greenColor()
var distToDestination: CGFloat = 10.0
var arcWidth: CGFloat = 20
var outlineWidth: CGFloat = 5
override func drawInContext(ctx: CGContext) {
super.drawInContext(ctx)
UIGraphicsPushContext(ctx)
//Drawing in the container
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let radius: CGFloat = max(bounds.width, bounds.height) - 10
let startAngle: CGFloat = 3 * CGFloat(M_PI) / 2
let endAngle: CGFloat = 3 * CGFloat(M_PI) / 2 + 2 * CGFloat(M_PI)
let path = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = arcWidth
backFillColor.setStroke()
path.stroke()
let fill = UIColor.blueColor().colorWithAlphaComponent(0.15)
fill.setFill()
path.fill()
//Drawing the fill path. Same process
let fillAngleLength = (CGFloat(M_PI)) * progress
let fillStartAngle = 3 * CGFloat(M_PI) / 2 - fillAngleLength
let fillEndAngle = 3 * CGFloat(M_PI) / 2 + fillAngleLength
let fillPath_fill = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: fillStartAngle, endAngle: fillEndAngle, clockwise: true)
fillPath_fill.lineWidth = arcWidth
fillColor.setStroke()
fillPath_fill.stroke()
//Drawing container outline on top
let outlinePath_outer = UIBezierPath(arcCenter: center, radius: radius / 2 - outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let outlinePath_inner = UIBezierPath(arcCenter: center, radius: radius / 2 - arcWidth + outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
outlinePath_outer.lineWidth = outlineWidth
outlinePath_inner.lineWidth = outlineWidth
strokeColor.setStroke()
outlinePath_outer.stroke()
outlinePath_inner.stroke()
UIGraphicsPopContext()
}
}
Whilst AntonTheDev provides a great answer, his solution does not allow you to animate the CircularProgressView in an animation block, so you cant do neat things like:
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut,
animations: {
circularProgress.progress = 0.76
}, completion: nil)
There's a similar question here with a up to date Swift 3 answer based on ideas from the accepted answer and this post.
This is what the final solution looks like.
Swift 3 Solution

Create spinning circle loading animation

I'm trying to create a spinning circle loader animation like in the following Android project (https://github.com/pedant/sweet-alert-dialog).
I don't need the entire popup dialog - just the spinning part. It changes colors and spins indefinitly (until I choose to dismiss it).
I'm kind of new to swift and I've never been the kind to do animations. Here's what I have so far (found code in similar project for iOS):
The layers setup:
outlineLayer.position = CGPointMake(0,
0);
outlineLayer.path = outlineCircle
outlineLayer.fillColor = UIColor.clearColor().CGColor;
outlineLayer.strokeColor = UIColor(red: 150.0/255.0, green: 216.0/255.0, blue: 115.0/255.0, alpha: 1.0).CGColor;
outlineLayer.lineCap = kCALineCapRound
outlineLayer.lineWidth = 4;
outlineLayer.opacity = 0.1
self.layer.addSublayer(outlineLayer)
circleLayer.position = CGPointMake(0,
0);
circleLayer.path = path
circleLayer.fillColor = UIColor.clearColor().CGColor;
circleLayer.strokeColor = UIColor(red: 150.0/255.0, green: 216.0/255.0, blue: 115.0/255.0, alpha: 1.0).CGColor;
circleLayer.lineCap = kCALineCapRound
circleLayer.lineWidth = 4;
circleLayer.actions = [
"strokeStart": NSNull(),
"strokeEnd": NSNull(),
"transform": NSNull()
]
self.layer.addSublayer(circleLayer)
Animation:
let strokeStart = CABasicAnimation(keyPath: "strokeStart")
let strokeEnd = CABasicAnimation(keyPath: "strokeEnd")
let factor = 0.545
let timing = CAMediaTimingFunction(controlPoints: 0.3, 0.6, 0.8, 1.2)
strokeEnd.fromValue = 0.00
strokeEnd.toValue = 0.93
strokeEnd.duration = 10.0 * factor
strokeEnd.timingFunction = timing
strokeEnd.autoreverses = true
strokeStart.fromValue = 0.0
strokeStart.toValue = 0.68
strokeStart.duration = 10.0 * factor
strokeStart.beginTime = CACurrentMediaTime() + 3.0 * factor
strokeStart.fillMode = kCAFillModeBackwards
strokeStart.timingFunction = timing
strokeStart.repeatCount = HUGE
circleLayer.strokeStart = 0.68
circleLayer.strokeEnd = 0.93
self.circleLayer.addAnimation(strokeEnd, forKey: "strokeEnd")
self.circleLayer.addAnimation(strokeStart, forKey: "strokeStart")
but what I have is not nearly close and I have no idea where to go from here. What I'm doing is changing a value and running seeing how it affects but I feel like I'm lost here.
How can I achieve such animation like in the example?
I didn't closely analyze the exact parameters of the animation, but this looks good to me:
import UIKit
#IBDesignable
class SpinnerView : UIView {
override var layer: CAShapeLayer {
get {
return super.layer as! CAShapeLayer
}
}
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override func layoutSubviews() {
super.layoutSubviews()
layer.fillColor = nil
layer.strokeColor = UIColor.black.cgColor
layer.lineWidth = 3
setPath()
}
override func didMoveToWindow() {
animate()
}
private func setPath() {
layer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: layer.lineWidth / 2, dy: layer.lineWidth / 2)).cgPath
}
struct Pose {
let secondsSincePriorPose: CFTimeInterval
let start: CGFloat
let length: CGFloat
init(_ secondsSincePriorPose: CFTimeInterval, _ start: CGFloat, _ length: CGFloat) {
self.secondsSincePriorPose = secondsSincePriorPose
self.start = start
self.length = length
}
}
class var poses: [Pose] {
get {
return [
Pose(0.0, 0.000, 0.7),
Pose(0.6, 0.500, 0.5),
Pose(0.6, 1.000, 0.3),
Pose(0.6, 1.500, 0.1),
Pose(0.2, 1.875, 0.1),
Pose(0.2, 2.250, 0.3),
Pose(0.2, 2.625, 0.5),
Pose(0.2, 3.000, 0.7),
]
}
}
func animate() {
var time: CFTimeInterval = 0
var times = [CFTimeInterval]()
var start: CGFloat = 0
var rotations = [CGFloat]()
var strokeEnds = [CGFloat]()
let poses = type(of: self).poses
let totalSeconds = poses.reduce(0) { $0 + $1.secondsSincePriorPose }
for pose in poses {
time += pose.secondsSincePriorPose
times.append(time / totalSeconds)
start = pose.start
rotations.append(start * 2 * .pi)
strokeEnds.append(pose.length)
}
times.append(times.last!)
rotations.append(rotations[0])
strokeEnds.append(strokeEnds[0])
animateKeyPath(keyPath: "strokeEnd", duration: totalSeconds, times: times, values: strokeEnds)
animateKeyPath(keyPath: "transform.rotation", duration: totalSeconds, times: times, values: rotations)
animateStrokeHueWithDuration(duration: totalSeconds * 5)
}
func animateKeyPath(keyPath: String, duration: CFTimeInterval, times: [CFTimeInterval], values: [CGFloat]) {
let animation = CAKeyframeAnimation(keyPath: keyPath)
animation.keyTimes = times as [NSNumber]?
animation.values = values
animation.calculationMode = .linear
animation.duration = duration
animation.repeatCount = Float.infinity
layer.add(animation, forKey: animation.keyPath)
}
func animateStrokeHueWithDuration(duration: CFTimeInterval) {
let count = 36
let animation = CAKeyframeAnimation(keyPath: "strokeColor")
animation.keyTimes = (0 ... count).map { NSNumber(value: CFTimeInterval($0) / CFTimeInterval(count)) }
animation.values = (0 ... count).map {
UIColor(hue: CGFloat($0) / CGFloat(count), saturation: 1, brightness: 1, alpha: 1).cgColor
}
animation.duration = duration
animation.calculationMode = .linear
animation.repeatCount = Float.infinity
layer.add(animation, forKey: animation.keyPath)
}
}
Try out my three custom Loader screen very simple :
Write below code in Viewcontoller.swift file
class ViewController: UIViewController {
var signView = SignView(frame: CGRect.zero)
var testView = TestView(frame: CGRect.zero)
var testView1 = TestView1(frame: CGRect.zero)
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.orange
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
addSignView()
//addTestView()
//addTestView1()
}
func addSignView() {
signView.frame = CGRect(x: 0,
y: 0,
width: UIScreen.main.bounds.size.width,
height: UIScreen.main.bounds.size.height)
self.view.addSubview(signView)
signView.addAnimationLayer()
}
func addTestView() {
let boxSize: CGFloat = 200.0
testView.frame = CGRect(x: 16,
y: 350,
width: boxSize,
height: boxSize)
self.view.addSubview(testView)
testView.addAnimationLayer()
}
func addTestView1() {
testView1.frame = CGRect(x: 0,
y: 0,
width: UIScreen.main.bounds.size.width,
height: UIScreen.main.bounds.size.height)
self.view.addSubview(testView1)
testView1.addAnimationLayer()
}}
Now Add 3 Files inherit with UiView named > SignView , TestView and TestView1
Code for SignView.swift file
class SignView: UIView {
let upCircleLayer = CAShapeLayer.init()
var path = UIBezierPath.init()
var animationDuration : Double = 2
var frameHeight : CGFloat = 50.0
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.black.withAlphaComponent(0.5)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var signWavePath : UIBezierPath {
var clockCycle = true
let yPoint = self.frame.size.height/2
frameHeight = self.frame.size.width/6
for x in 1...24{
if x%2 != 0 {
let xpath = UIBezierPath(arcCenter: CGPoint(x: CGFloat(x)*frameHeight/2, y: yPoint),
radius: frameHeight/2,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: clockCycle)
path.append(xpath)
if(clockCycle){
clockCycle = false
}
else{
clockCycle = true
}
}
}
return path;
}
func addAnimationLayer() {
// Add Upper Circle Layer
upCircleLayer.fillColor = UIColor.clear.cgColor
upCircleLayer.strokeColor = UIColor.white.cgColor
upCircleLayer.lineWidth = 8.0
upCircleLayer.path = signWavePath.cgPath
layer.addSublayer(upCircleLayer)
animateStrokeUpCircle()
Timer.scheduledTimer(timeInterval: animationDuration, target: self, selector: #selector(animateStrokeUpCircle), userInfo: nil, repeats: true)
}
func animateStrokeUpCircle() {
let strokeAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeAnimation.fromValue = 0.0
strokeAnimation.toValue = 1.0
strokeAnimation.duration = animationDuration
strokeAnimation.isRemovedOnCompletion = false
upCircleLayer.add(strokeAnimation, forKey: nil)
expand1()
}
func expand1() {
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "position")
expandAnimation.fromValue = [0,sin(self.frame.width)]
expandAnimation.toValue = [-self.frame.width,cos(self.frame.width)]
expandAnimation.duration = animationDuration
expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
upCircleLayer.add(expandAnimation, forKey: nil)
}
}
Code for TestView File :
class TestView: UIView {
let upCircleLayer = CAShapeLayer.init()
let downCircleLayer = CAShapeLayer.init()
var path1 = UIBezierPath.init()
var path2 = UIBezierPath.init()
var animationDirection : Bool = true
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var up1Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
}
var down2Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: 3*self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
}
var up22Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: 3*self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
}
var down11Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
}
var up2Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: 3*self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 0.0,
endAngle: 180.0 * .pi/180.0,
clockwise: true)
}
var down1Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 0.0,
endAngle: 180.0 * .pi/180.0,
clockwise: false)
}
func addAnimationLayer() {
path1.append(up1Circle);
path1.append(down2Circle);
path2.append(down11Circle)
path2.append(up22Circle)
// Add Upper Circle Layer
upCircleLayer.fillColor = UIColor.clear.cgColor
upCircleLayer.strokeColor = UIColor.black.cgColor
upCircleLayer.lineWidth = 8.0
upCircleLayer.path = path1.cgPath
layer.addSublayer(upCircleLayer)
Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(expand1), userInfo: nil, repeats: true)
}
func expand() {
if animationDirection{
//upCircleLayer.path = path1.cgPath
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expandAnimation.fromValue = path1.cgPath
expandAnimation.toValue = path2.cgPath
expandAnimation.duration = 1.5
//expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
upCircleLayer.add(expandAnimation, forKey: nil)
animationDirection = false
}
else{
//upCircleLayer.path = path2.cgPath
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expandAnimation.fromValue = path2.cgPath
expandAnimation.toValue = path1.cgPath
expandAnimation.duration = 1.5
//expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
upCircleLayer.add(expandAnimation, forKey: nil)
animationDirection = true
}
}
func expand1() {
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "position")
expandAnimation.fromValue = [0,self.frame.height/2]
expandAnimation.toValue = 500
expandAnimation.duration = 2.0
expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
upCircleLayer.add(expandAnimation, forKey: nil)
}
}
And Code for TestView1.swift file
class TestView1: UIView {
let animationLayer = CAShapeLayer.init()
var path1 = UIBezierPath.init()
var path2 = UIBezierPath.init()
var path = UIBezierPath.init()
var circleRadius : CGFloat = 26.0;
var centerLineHeight : CGFloat = 40.0
var animationDuration : Double = 2.0
var animationDirection : Bool = true
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.black
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var centerMainLine: UIBezierPath {
let frameSize = self.frame.size
let centerLine = UIBezierPath()
centerLine.move(to: CGPoint(x: frameSize.width/2, y: frameSize.height/2 - centerLineHeight/2))
centerLine.addLine(to: CGPoint(x: frameSize.width/2, y: frameSize.height/2 + centerLineHeight/2))
return centerLine
}
var upLeftCircle: UIBezierPath {
let frameSize = self.frame.size
let halfCircle = UIBezierPath(arcCenter: CGPoint(x: frameSize.width/2 - circleRadius, y: frameSize.height/2 - centerLineHeight/2),
radius: circleRadius,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
return halfCircle
}
var upRightCircle: UIBezierPath {
let frameSize = self.frame.size
let halfCircle = UIBezierPath(arcCenter: CGPoint(x: frameSize.width/2 + circleRadius, y: frameSize.height/2 - centerLineHeight/2),
radius: circleRadius,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
return halfCircle
}
var downLeftCircle: UIBezierPath {
let frameSize = self.frame.size
let halfCircle = UIBezierPath(arcCenter: CGPoint(x: frameSize.width/2 - circleRadius, y: frameSize.height/2 + centerLineHeight/2),
radius: circleRadius,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
return halfCircle
}
var downRightCircle: UIBezierPath {
let frameSize = self.frame.size
let halfCircle = UIBezierPath(arcCenter: CGPoint(x: frameSize.width/2 + circleRadius, y: frameSize.height/2 + centerLineHeight/2),
radius: circleRadius,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
return halfCircle
}
func drawUpCircle(centerPoint:CGPoint, radiusValue:CGFloat) -> UIBezierPath {
let halfCircle = UIBezierPath(arcCenter: centerPoint,
radius: radiusValue,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
return halfCircle
}
func drawDownCircle(centerPoint:CGPoint,radiusValue:CGFloat) -> UIBezierPath {
let halfCircle = UIBezierPath(arcCenter: centerPoint,
radius: radiusValue,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
return halfCircle
}
func drawLine(fromPoint:CGPoint,toPoint:CGPoint) -> UIBezierPath {
let line = UIBezierPath()
line.move(to: fromPoint)
line.addLine(to: toPoint)
return line
}
func addAnimationLayer() {
createPathOne()
createPathTwo()
createPath()
// set Animation Layer design
animationLayer.fillColor = UIColor.clear.cgColor
animationLayer.strokeColor = UIColor.white.cgColor
animationLayer.lineWidth = 8.0
animationLayer.path = path.cgPath
layer.addSublayer(animationLayer)
expand1()
Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(expand1), userInfo: nil, repeats: true)
}
func expand1() {
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "position")
expandAnimation.fromValue = [0,0]
expandAnimation.toValue = [-2000,0]
expandAnimation.duration = 10.0
expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
animationLayer.add(expandAnimation, forKey: nil)
}
func expand() {
animationLayer.path = centerMainLine.cgPath
if animationDirection{
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expandAnimation.fromValue = path1.cgPath
expandAnimation.toValue = path2.cgPath
expandAnimation.duration = animationDuration
expandAnimation.fillMode = kCAFillModeBackwards
expandAnimation.isRemovedOnCompletion = false
animationLayer.add(expandAnimation, forKey: nil)
animationDirection = false
}
else{
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expandAnimation.fromValue = path2.cgPath
expandAnimation.toValue = path1.cgPath
expandAnimation.duration = animationDuration
expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
animationLayer.add(expandAnimation, forKey: nil)
animationDirection = true
}
}
func createPathOne(){
path1.append(upLeftCircle);
path1.append(centerMainLine);
path1.append(downRightCircle)
}
func createPathTwo(){
path2.append(downLeftCircle);
path2.append(centerMainLine);
path2.append(upRightCircle)
}
func createPath() {
let frameSize = self.frame.size;
let lineHeight1 : CGFloat = 30
let lineHeight2 : CGFloat = 20
let radius1 : CGFloat = 40.0
let radius2 : CGFloat = 20.0
var lastPoint : CGPoint = CGPoint(x:0.0,y:frameSize.height/2 - lineHeight1/2)
for i in 1...10{
let p1 = drawUpCircle(centerPoint: CGPoint(x: lastPoint.x + radius1, y: lastPoint.y ), radiusValue: radius1)
lastPoint = p1.currentPoint;
let p2 = drawLine(fromPoint: lastPoint , toPoint: CGPoint(x:lastPoint.x, y: lastPoint.y+lineHeight1))
lastPoint = p2.currentPoint;
let p3 = drawDownCircle(centerPoint: CGPoint(x:lastPoint.x + radius1, y: lastPoint.y), radiusValue: radius1)
lastPoint = p3.currentPoint;
let p4 = drawLine(fromPoint: lastPoint, toPoint: CGPoint(x:lastPoint.x, y: lastPoint.y - lineHeight2))
lastPoint = p4.currentPoint;
let p5 = drawUpCircle(centerPoint: CGPoint(x:lastPoint.x + radius2, y: lastPoint.y), radiusValue: radius2)
lastPoint = p5.currentPoint;
let p6 = drawLine(fromPoint: lastPoint, toPoint: CGPoint(x:lastPoint.x, y: lastPoint.y + lineHeight2))
lastPoint = p6.currentPoint;
let p7 = drawDownCircle(centerPoint: CGPoint(x:lastPoint.x + radius2, y: lastPoint.y), radiusValue: radius2)
lastPoint = p7.currentPoint
let p8 = drawLine(fromPoint: lastPoint, toPoint: CGPoint(x:lastPoint.x, y: lastPoint.y - lineHeight1))
lastPoint = p8.currentPoint;
path.append(p1)
path.append(p2)
path.append(p3)
path.append(p4)
path.append(p5)
path.append(p6)
path.append(p7)
path.append(p8)
}
}
}
Now run the code to check the Animation Loader. Comment/Uncomment other 2 loader method in viewDidAppear method of viewcontroller.
Enjoy!!
If someone is looking for Objective C version of #rob mayoff solution
in SpinnerView.h
#import <UIKit/UIKit.h>
IB_DESIGNABLE
#interface SpinnerView : UIView
#end
in SpinnerView.m
#import "SpinnerView.h"
#import "Pose.h"
#implementation SpinnerView
- (instancetype) initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
return self;
}
- (instancetype) initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
return self;
}
- (CAShapeLayer*) layer {
return (CAShapeLayer*)super.layer;
}
- (CAShapeLayer*) getLayer{
return (CAShapeLayer*)super.layer;
}
+ (Class)layerClass{
return [CAShapeLayer class];
}
- (void) layoutSubviews{
[super layoutSubviews];
[self getLayer].fillColor = nil;
[self getLayer].strokeColor = [UIColor blackColor].CGColor;
[self getLayer].lineWidth = 3;
[self setPath];
}
- (void) didMoveToWindow{
[self animate];
}
- (void) setPath{
UIBezierPath* bezierPath = ([UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, [self getLayer].lineWidth/2, [self getLayer].lineWidth/2)]);
[self getLayer].path = bezierPath.CGPath;
}
- (NSArray*) poses{
NSMutableArray* poses = [[NSMutableArray alloc] init];
[poses addObject:[[Pose alloc] initWith:0.0 start:0.000 length:0.7]];
[poses addObject:[[Pose alloc] initWith:0.6 start:0.500 length:0.5]];
[poses addObject:[[Pose alloc] initWith:0.6 start:1.000 length:0.3]];
[poses addObject:[[Pose alloc] initWith:0.6 start:1.500 length:0.1]];
[poses addObject:[[Pose alloc] initWith:0.2 start:1.875 length:0.1]];
[poses addObject:[[Pose alloc] initWith:0.2 start:2.250 length:0.3]];
[poses addObject:[[Pose alloc] initWith:0.2 start:2.625 length:0.7]];
[poses addObject:[[Pose alloc] initWith:0.2 start:3.000 length:0.5]];
return poses;
}
- (void) animate{
CFTimeInterval time = 0;
NSMutableArray* times = [NSMutableArray new];;
CGFloat start = 0;
NSMutableArray* rotations = [NSMutableArray new];
NSMutableArray* strokeEnds = [NSMutableArray new];
NSArray* posses = [self poses];
double totalSeconds = [[posses valueForKeyPath:#"#sum.secondsSincePriorPose"] doubleValue];
for(Pose* pose in posses){
time += pose.secondsSincePriorPose;
[times addObject:[NSNumber numberWithDouble:time/totalSeconds]];
start = pose.start;
[rotations addObject:[NSNumber numberWithDouble:start*2*M_PI]];
[strokeEnds addObject:[NSNumber numberWithDouble:pose.length]];
}
[times addObject:[times lastObject]];
[rotations addObject:[rotations firstObject]];
[strokeEnds addObject:[strokeEnds firstObject]];
[self animateKeyPath:#"strokeEnd" duration:totalSeconds times:times values:strokeEnds];
[self animateKeyPath:#"transform.rotation" duration:totalSeconds times:times values:rotations];
[self animateStrokeHueWithDuration:totalSeconds * 5];
}
- (void) animateKeyPath:(NSString*)keyPath duration:(CFTimeInterval)duration times:(NSArray*)times values:(NSArray*)values{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:keyPath];
animation.keyTimes = times;
animation.values = values;
animation.calculationMode = kCAAnimationLinear;
animation.duration = duration;
animation.repeatCount = FLT_MAX;
[[self getLayer] addAnimation:animation forKey:animation.keyPath];
}
- (void) animateStrokeHueWithDuration:(CFTimeInterval)duration{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:#"strokeColor"];
NSMutableArray *keyTimes = [NSMutableArray array];
NSMutableArray *values = [NSMutableArray array];
for (NSInteger i = 0; i < 36; i++) {
[keyTimes addObject: [NSNumber numberWithDouble:(CFTimeInterval)i/(CFTimeInterval)36]];
[values addObject:(id)[UIColor colorWithHue:(CGFloat)i/(CGFloat)36 saturation:1 brightness:1 alpha:1].CGColor];
}
animation.keyTimes = keyTimes;
animation.values = values;
animation.calculationMode = kCAAnimationLinear;
animation.duration = duration;
animation.repeatCount = FLT_MAX;
[[self getLayer] addAnimation:animation forKey:animation.keyPath];
}
#end
Pose.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#interface Pose : NSObject
#property CFTimeInterval secondsSincePriorPose;
#property CGFloat start;
#property CGFloat length;
- (instancetype) initWith:(CFTimeInterval)timeInterval start:(CGFloat)start length:(CGFloat)length;
#end
Pose.m
#import "Pose.h"
#import <UIKit/UIKit.h>
#implementation Pose
- (instancetype) initWith:(CFTimeInterval)timeInterval start:(CGFloat)start length:(CGFloat)length{
self = [super init];
self.start = start;
self.length = length;
self.secondsSincePriorPose = timeInterval;
return self;
}
#end

Resources