Create spinning circle loading animation - ios
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
Related
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) } }
UIBezierPath Custom lineCapStyle different on both Ends
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 } }
Custom indicator with rotate blink animation like UIActivityIndicatorView
I am trying to make custom activity indicator, see the indicator class below import UIKit class MyIndicator: UIView { let gap = CGFloat(.pi/4 / 6.0) var count = 0 override func draw(_ rect: CGRect) { super.draw(rect) } func blink() { backgroundColor = .clear let duration: CFTimeInterval = 1.2 //let beginTime = CACurrentMediaTime() let beginTimes: [CFTimeInterval] = [0.25, 1, 1.75, 2.5] let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) // Animation let animation = CAKeyframeAnimation(keyPath: "opacity") animation.keyTimes = [0, 0.5, 1] animation.timingFunctions = [timingFunction, timingFunction] animation.values = [1, 0.3, 1] animation.duration = duration animation.repeatCount = HUGE animation.isRemovedOnCompletion = false for i in 0...3 { let shape = CAShapeLayer() shape.frame = self.bounds shape.fillColor = UIColor.clear.cgColor shape.lineWidth = 6.8 shape.strokeColor = UIColor.blue.cgColor let startAngle:CGFloat = CGFloat(i) * CGFloat(Double.pi/2) + gap let endAngle:CGFloat = startAngle + CGFloat(Double.pi/2) - gap * 2 shape.path = UIBezierPath(arcCenter: center, radius: -20, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath animation.beginTime = beginTimes[i] shape.add(animation, forKey: "animation") self.layer.addSublayer(shape) } } func startAnimating() { blink() } } let indicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) self.view.addSubview(indicator) indicator.startAnimating() I have attached my current result. But you can see that the animation is not in circular motion like standard UIActivityIndicatorView. Can anyone help me to fix this.
Try using a CAReplicatorLayer and instance delay to get everything in sync. Here is a Playground. I am not 100% sure on the visual you want but this should be close. //: A UIKit based Playground for presenting user interface import UIKit import PlaygroundSupport class MyIndicator: UIView { let gap = CGFloat(.pi/4 / 6.0) private var replicatorLayer = CAReplicatorLayer() private var mainShapeLayer = CAShapeLayer() override init(frame: CGRect) { super.init(frame: frame) commonSetup() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) commonSetup() } func commonSetup(){ mainShapeLayer = CAShapeLayer() mainShapeLayer.frame = self.bounds mainShapeLayer.fillColor = UIColor.clear.cgColor mainShapeLayer.lineWidth = 6.8 mainShapeLayer.strokeColor = UIColor.blue.cgColor let startAngle:CGFloat = CGFloat(Double.pi * 2) + gap/2 let endAngle:CGFloat = startAngle + CGFloat(Double.pi/2) - gap/2 mainShapeLayer.path = UIBezierPath(arcCenter: center, radius: self.bounds.midX - 10, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath replicatorLayer = CAReplicatorLayer() replicatorLayer.frame = self.bounds replicatorLayer.instanceCount = 4 let angle = (Double.pi * 2)/4 replicatorLayer.instanceTransform = CATransform3DRotate(CATransform3DIdentity, CGFloat(angle), 0, 0, 1) replicatorLayer.addSublayer(mainShapeLayer) replicatorLayer.opacity = 0 self.layer.addSublayer(replicatorLayer) } func animate(){ let defaultDuration : Double = 0.75 let animate = CAKeyframeAnimation(keyPath: "opacity") animate.values = [1, 0.3, 1] animate.keyTimes = [0, 0.5, 1] animate.repeatCount = .greatestFiniteMagnitude animate.duration = defaultDuration animate.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) replicatorLayer.instanceDelay = defaultDuration/4 self.mainShapeLayer.add(animate, forKey: nil) let opacityIn = CABasicAnimation(keyPath: "opacity") opacityIn.fromValue = 1 opacityIn.toValue = 0 opacityIn.duration = 0.2 replicatorLayer.add(opacityIn, forKey: nil) self.replicatorLayer.opacity = 1 } func stopAnimating(){ CATransaction.begin() let opacityOut = CABasicAnimation(keyPath: "opacity") opacityOut.fromValue = 1 opacityOut.toValue = 0 opacityOut.duration = 0.2 CATransaction.setCompletionBlock { [weak self] in self?.mainShapeLayer.removeAllAnimations() } replicatorLayer.add(opacityOut, forKey: nil) self.replicatorLayer.opacity = 0 CATransaction.commit() } override func layoutSubviews() { super.layoutSubviews() mainShapeLayer.frame = self.bounds replicatorLayer.frame = self.bounds } } class MyViewController : UIViewController { override func loadView() { let view = UIView() view.backgroundColor = .white let indicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) indicator.animate() //just to simulate starting and stoping DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 10) { indicator.stopAnimating() DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) { indicator.animate() } } view.addSubview(indicator) self.view = view } } // Present the view controller in the Live View window PlaygroundPage.current.liveView = MyViewController()
iOS - Draw a donut in an view controller
This is really simple, but I can't figure out what I'm doing wrong. I have a view controller with one view in the center. I want to draw something like the circle below: The main problem I'm having, is I can't get anytihng to show up on the view. I'm just trying to draw a line right now, but am obviously missing something key. Here is my code: #interface ViewController () #property (weak, nonatomic) IBOutlet UIView *centerView; #end #implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [[UIColor brownColor] set]; CGContextRef currentContext =UIGraphicsGetCurrentContext(); CGContextSetLineWidth(currentContext,5.0f); CGContextMoveToPoint(currentContext,50.0f, 10.0f); CGContextAddLineToPoint(currentContext,100.0f, 200.0f); CGContextStrokePath(currentContext); }
You can do it with CAShapeLayer. Here is a playground that illustrates how to do it. As an added bonus, I threw in the animation. import UIKit import PlaygroundSupport class AnimatedRingView: UIView { private static let animationDuration = CFTimeInterval(2) private let π = CGFloat.pi private let startAngle = 1.5 * CGFloat.pi private let strokeWidth = CGFloat(8) var proportion = CGFloat(0.5) { didSet { setNeedsLayout() } } private lazy var circleLayer: CAShapeLayer = { let circleLayer = CAShapeLayer() circleLayer.strokeColor = UIColor.gray.cgColor circleLayer.fillColor = UIColor.clear.cgColor circleLayer.lineWidth = self.strokeWidth self.layer.addSublayer(circleLayer) return circleLayer }() private lazy var ringlayer: CAShapeLayer = { let ringlayer = CAShapeLayer() ringlayer.fillColor = UIColor.clear.cgColor ringlayer.strokeColor = self.tintColor.cgColor ringlayer.lineCap = kCALineCapRound ringlayer.lineWidth = self.strokeWidth self.layer.addSublayer(ringlayer) return ringlayer }() override func layoutSubviews() { super.layoutSubviews() let radius = (min(frame.size.width, frame.size.height) - strokeWidth - 2)/2 let circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: startAngle + 2 * π, clockwise: true) circleLayer.path = circlePath.cgPath ringlayer.path = circlePath.cgPath ringlayer.strokeEnd = proportion } func animateRing(From startProportion: CGFloat, To endProportion: CGFloat, Duration duration: CFTimeInterval = animationDuration) { let animation = CABasicAnimation(keyPath: "strokeEnd") animation.duration = duration animation.fromValue = startProportion animation.toValue = endProportion animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) ringlayer.strokeEnd = endProportion ringlayer.strokeStart = startProportion ringlayer.add(animation, forKey: "animateRing") } } let v = AnimatedRingView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) PlaygroundPage.current.liveView = v v.animateRing(From: 0, To: 0.5)
iOS - Circle shaped gradient
I am trying to draw a circular shaped gradient. let backgroundView:UIView = UIView() let backgroundLayer:CAShapeLayer = CAShapeLayer() let gradient:CAGradientLayer = CAGradientLayer() ... backgroundLayer.frame = CGRectMake(0, 0, backgroundDiameter, backgroundDiameter) backgroundLayer.backgroundColor = UIColor.clearColor().CGColor backgroundLayer.strokeColor = backgroundStrokeColor backgroundLayer.fillColor = backgroundFillColor gradient.colors = [UIColor(red: 0.5, green: 0.5, blue: 0.9, alpha: 1.0).CGColor, UIColor(red: 0.9, green: 0.9, blue: 0.3, alpha: 1.0).CGColor] gradient.locations = [0.01, 0.8] gradient.frame = backgroundLayer.frame backgroundView.frame = CGRectMake(0, 0, backgroundDiameter, backgroundDiameter) backgroundView.backgroundColor = UIColor.clearColor() backgroundView.center = ringControlCenter backgroundLayer.insertSublayer(gradient, atIndex: 1) backgroundLayer.path = CGPathCreateWithEllipseInRect(backgroundLayer.frame, nil) backgroundView.layer.addSublayer(backgroundLayer) self.addSubview(backgroundView) However the gradient does not seem to be impacted by: backgroundLayer.path = CGPathCreateWithEllipseInRect(backgroundLayer.frame, nil) And still have its initial shape. Is there a way to mask the gradient with an ellipse shaped layer without using CGContext* instructions? Thank you, MG
I've translated Leo library's in Swift 3.x , in other words , the good WCGradientCircleLayer writed in objective-C (it work exactly as aspected) import UIKit class WCGraintCircleLayer: CALayer { override init () { super.init() } convenience init(bounds:CGRect,position:CGPoint,fromColor:UIColor,toColor:UIColor,linewidth:CGFloat,toValue:CGFloat) { self.init() self.bounds = bounds self.position = position let colors : [UIColor] = self.graint(fromColor: fromColor, toColor:toColor, count:4) for i in 0..<colors.count-1 { let graint = CAGradientLayer() graint.bounds = CGRect(origin:CGPoint.zero, size: CGSize(width:bounds.width/2,height:bounds.height/2)) let valuePoint = self.positionArrayWith(bounds: self.bounds)[i] graint.position = valuePoint print("iesimo graint position: \(graint.position)") let fromColor = colors[i] let toColor = colors[i+1] let colors : [CGColor] = [fromColor.cgColor,toColor.cgColor] let stopOne: CGFloat = 0.0 let stopTwo: CGFloat = 1.0 let locations : [CGFloat] = [stopOne,stopTwo] graint.colors = colors graint.locations = locations as [NSNumber]? // with Swift 2 and Swift 3 you can cast directly a `CGFloat` value to `NSNumber` and back graint.startPoint = self.startPoints()[i] graint.endPoint = self.endPoints()[i] self.addSublayer(graint) //Set mask let shapelayer = CAShapeLayer() let rect = CGRect(origin:CGPoint.zero,size:CGSize(width:self.bounds.width - 2 * linewidth,height: self.bounds.height - 2 * linewidth)) shapelayer.bounds = rect shapelayer.position = CGPoint(x:self.bounds.width/2,y: self.bounds.height/2) shapelayer.strokeColor = UIColor.blue.cgColor shapelayer.fillColor = UIColor.clear.cgColor shapelayer.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.width/2).cgPath shapelayer.lineWidth = linewidth shapelayer.lineCap = kCALineCapRound shapelayer.strokeStart = 0.010 let finalValue = (toValue*0.99) shapelayer.strokeEnd = finalValue//0.99; self.mask = shapelayer } } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func layerWithWithBounds(bounds:CGRect, position:CGPoint, fromColor:UIColor, toColor:UIColor, linewidth : CGFloat,toValue:CGFloat) -> WCGraintCircleLayer { let layer = WCGraintCircleLayer(bounds: bounds,position: position,fromColor:fromColor, toColor: toColor,linewidth: linewidth,toValue:toValue ) return layer } func graint(fromColor:UIColor, toColor:UIColor, count:Int) -> [UIColor]{ var fromR:CGFloat = 0.0,fromG:CGFloat = 0.0,fromB:CGFloat = 0.0,fromAlpha:CGFloat = 0.0 fromColor.getRed(&fromR,green: &fromG,blue: &fromB,alpha: &fromAlpha) var toR:CGFloat = 0.0,toG:CGFloat = 0.0,toB:CGFloat = 0.0,toAlpha:CGFloat = 0.0 toColor.getRed(&toR,green: &toG,blue: &toB,alpha: &toAlpha) var result : [UIColor]! = [UIColor]() for i in 0...count { let oneR:CGFloat = fromR + (toR - fromR)/CGFloat(count) * CGFloat(i) let oneG : CGFloat = fromG + (toG - fromG)/CGFloat(count) * CGFloat(i) let oneB : CGFloat = fromB + (toB - fromB)/CGFloat(count) * CGFloat(i) let oneAlpha : CGFloat = fromAlpha + (toAlpha - fromAlpha)/CGFloat(count) * CGFloat(i) let oneColor = UIColor.init(red: oneR, green: oneG, blue: oneB, alpha: oneAlpha) result.append(oneColor) print(oneColor) } return result } func positionArrayWith(bounds:CGRect) -> [CGPoint]{ let first = CGPoint(x:(bounds.width/4)*3,y: (bounds.height/4)*1) let second = CGPoint(x:(bounds.width/4)*3,y: (bounds.height/4)*3) let third = CGPoint(x:(bounds.width/4)*1,y: (bounds.height/4)*3) let fourth = CGPoint(x:(bounds.width/4)*1,y: (bounds.height/4)*1) print([first,second,third,fourth]) return [first,second,third,fourth] } func startPoints() -> [CGPoint] { return [CGPoint.zero,CGPoint(x:1,y:0),CGPoint(x:1,y:1),CGPoint(x:0,y:1)] } func endPoints() -> [CGPoint] { return [CGPoint(x:1,y:1),CGPoint(x:0,y:1),CGPoint.zero,CGPoint(x:1,y:0)] } func midColorWithFromColor(fromColor:UIColor, toColor:UIColor, progress:CGFloat) -> UIColor { var fromR:CGFloat = 0.0,fromG:CGFloat = 0.0,fromB:CGFloat = 0.0,fromAlpha:CGFloat = 0.0 fromColor.getRed(&fromR,green: &fromG,blue: &fromB,alpha: &fromAlpha) var toR:CGFloat = 0.0,toG:CGFloat = 0.0,toB:CGFloat = 0.0,toAlpha:CGFloat = 0.0 toColor.getRed(&toR,green: &toG,blue: &toB,alpha: &toAlpha) let oneR = fromR + (toR - fromR) * progress let oneG = fromG + (toG - fromG) * progress let oneB = fromB + (toB - fromB) * progress let oneAlpha = fromAlpha + (toAlpha - fromAlpha) * progress let oneColor = UIColor.init(red: oneR, green: oneG, blue: oneB, alpha: oneAlpha) return oneColor } // This is what you call if you want to draw a full circle. func animateCircle(duration: TimeInterval) { animateCircleTo(duration: duration, fromValue: 0.010, toValue: 0.99) } // This is what you call to draw a partial circle. func animateCircleTo(duration: TimeInterval, fromValue: CGFloat, toValue: CGFloat){ // We want to animate the strokeEnd property of the circleLayer let animation = CABasicAnimation(keyPath: "strokeEnd") animation.isRemovedOnCompletion = true // Set the animation duration appropriately animation.duration = duration // Animate from 0.010 (no circle) to 0.99 (full circle) animation.fromValue = 0.010 animation.toValue = toValue // Do an easeout. Don't know how to do a spring instead //animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) // Set the circleLayer's strokeEnd property to 0.99 now so that it's the // right value when the animation ends. let circleMask = self.mask as! CAShapeLayer circleMask.strokeEnd = toValue // Do the actual animation circleMask.removeAllAnimations() circleMask.add(animation, forKey: "animateCircle") } } And this is a little example, how to use in Swift 3.x: let gradientRingLayer = WCGraintCircleLayer(bounds: CGRect(origin: CGPoint.zero,size:CGSize(width: 150, height: 150)), position:CGPoint(x: 200, y: 300),fromColor:UIColor.blue, toColor:UIColor.white, linewidth:4.0, toValue:0) self.view.layer.addSublayer(gradientRingLayer) let duration = 3.0 gradientRingLayer.animateCircleTo(duration: duration, fromValue: 0, toValue: 0.99) This is the code in Swift 2.x: import UIKit class WCGraintCircleLayer: CALayer { override init () { super.init() } convenience init(bounds:CGRect,position:CGPoint,fromColor:UIColor,toColor:UIColor,linewidth:CGFloat,toValue:CGFloat) { self.init() self.bounds = bounds self.position = position let colors : [UIColor] = self.graintFromColor(fromColor, toColor:toColor, count:4) for i in 0..<colors.count-1 { let graint = CAGradientLayer() graint.bounds = CGRectMake(0,0,CGRectGetWidth(bounds)/2,CGRectGetHeight(bounds)/2) let valuePoint = self.positionArrayWithMainBounds(self.bounds)[i] graint.position = valuePoint print("iesimo graint position: \(graint.position)") let fromColor = colors[i] let toColor = colors[i+1] let colors : [CGColorRef] = [fromColor.CGColor,toColor.CGColor] let stopOne: CGFloat = 0.0 let stopTwo: CGFloat = 1.0 let locations : [CGFloat] = [stopOne,stopTwo] graint.colors = colors graint.locations = locations graint.startPoint = self.startPoints()[i] graint.endPoint = self.endPoints()[i] self.addSublayer(graint) //Set mask let shapelayer = CAShapeLayer() let rect = CGRectMake(0,0,CGRectGetWidth(self.bounds) - 2 * linewidth, CGRectGetHeight(self.bounds) - 2 * linewidth) shapelayer.bounds = rect shapelayer.position = CGPointMake(CGRectGetWidth(self.bounds)/2, CGRectGetHeight(self.bounds)/2) shapelayer.strokeColor = UIColor.blueColor().CGColor shapelayer.fillColor = UIColor.clearColor().CGColor shapelayer.path = UIBezierPath(roundedRect: rect, cornerRadius: CGRectGetWidth(rect)/2).CGPath shapelayer.lineWidth = linewidth shapelayer.lineCap = kCALineCapRound shapelayer.strokeStart = 0.010 let finalValue = (toValue*0.99) shapelayer.strokeEnd = finalValue//0.99; self.mask = shapelayer } } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func layerWithWithBounds(bounds:CGRect, position:CGPoint, fromColor:UIColor, toColor:UIColor, linewidth : CGFloat,toValue:CGFloat) -> WCGraintCircleLayer { let layer = WCGraintCircleLayer(bounds: bounds,position: position,fromColor:fromColor, toColor: toColor,linewidth: linewidth,toValue:toValue ) return layer } func graintFromColor(fromColor:UIColor, toColor:UIColor, count:Int) -> [UIColor]{ var fromR:CGFloat = 0.0,fromG:CGFloat = 0.0,fromB:CGFloat = 0.0,fromAlpha:CGFloat = 0.0 fromColor.getRed(&fromR,green: &fromG,blue: &fromB,alpha: &fromAlpha) var toR:CGFloat = 0.0,toG:CGFloat = 0.0,toB:CGFloat = 0.0,toAlpha:CGFloat = 0.0 toColor.getRed(&toR,green: &toG,blue: &toB,alpha: &toAlpha) var result : [UIColor]! = [UIColor]() for i in 0...count { let oneR:CGFloat = fromR + (toR - fromR)/CGFloat(count) * CGFloat(i) let oneG : CGFloat = fromG + (toG - fromG)/CGFloat(count) * CGFloat(i) let oneB : CGFloat = fromB + (toB - fromB)/CGFloat(count) * CGFloat(i) let oneAlpha : CGFloat = fromAlpha + (toAlpha - fromAlpha)/CGFloat(count) * CGFloat(i) let oneColor = UIColor.init(red: oneR, green: oneG, blue: oneB, alpha: oneAlpha) result.append(oneColor) print(oneColor) } return result } func positionArrayWithMainBounds(bounds:CGRect) -> [CGPoint]{ let first = CGPointMake((CGRectGetWidth(bounds)/4)*3, (CGRectGetHeight(bounds)/4)*1) let second = CGPointMake((CGRectGetWidth(bounds)/4)*3, (CGRectGetHeight(bounds)/4)*3) let third = CGPointMake((CGRectGetWidth(bounds)/4)*1, (CGRectGetHeight(bounds)/4)*3) let fourth = CGPointMake((CGRectGetWidth(bounds)/4)*1, (CGRectGetHeight(bounds)/4)*1) print([first,second,third,fourth]) return [first,second,third,fourth] } func startPoints() -> [CGPoint] { return [CGPointMake(0,0),CGPointMake(1,0),CGPointMake(1,1),CGPointMake(0,1)] } func endPoints() -> [CGPoint] { return [CGPointMake(1,1),CGPointMake(0,1),CGPointMake(0,0),CGPointMake(1,0)] } func midColorWithFromColor(fromColor:UIColor, toColor:UIColor, progress:CGFloat) -> UIColor { var fromR:CGFloat = 0.0,fromG:CGFloat = 0.0,fromB:CGFloat = 0.0,fromAlpha:CGFloat = 0.0 fromColor.getRed(&fromR,green: &fromG,blue: &fromB,alpha: &fromAlpha) var toR:CGFloat = 0.0,toG:CGFloat = 0.0,toB:CGFloat = 0.0,toAlpha:CGFloat = 0.0 toColor.getRed(&toR,green: &toG,blue: &toB,alpha: &toAlpha) let oneR = fromR + (toR - fromR) * progress let oneG = fromG + (toG - fromG) * progress let oneB = fromB + (toB - fromB) * progress let oneAlpha = fromAlpha + (toAlpha - fromAlpha) * progress let oneColor = UIColor.init(red: oneR, green: oneG, blue: oneB, alpha: oneAlpha) return oneColor } // This is what you call if you want to draw a full circle. func animateCircle(duration: NSTimeInterval) { animateCircleTo(duration, fromValue: 0.010, toValue: 0.99) } // This is what you call to draw a partial circle. func animateCircleTo(duration: NSTimeInterval, fromValue: CGFloat, toValue: CGFloat){ // We want to animate the strokeEnd property of the circleLayer let animation = CABasicAnimation(keyPath: "strokeEnd") animation.removedOnCompletion = true // Set the animation duration appropriately animation.duration = duration // Animate from 0.010 (no circle) to 0.99 (full circle) animation.fromValue = 0.010 animation.toValue = toValue // Do an easeout. Don't know how to do a spring instead //animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) // Set the circleLayer's strokeEnd property to 0.99 now so that it's the // right value when the animation ends. let circleMask = self.mask as! CAShapeLayer circleMask.strokeEnd = toValue // Do the actual animation circleMask.removeAllAnimations() circleMask.addAnimation(animation, forKey: "animateCircle") } } And this is a little example, how to use in Swift 2.x: let gradientRingLayer = WCGraintCircleLayer(bounds: CGRectMake(0, 0, 150, 150), position:CGPointMake(200,300) ,fromColor:UIColor.blueColor(), toColor:UIColor.whiteColor(),linewidth:4.0, toValue:0) self.view.layer.addSublayer(gradientRingLayer) let duration = 3.0 gradientRingLayer.animateCircleTo(duration, fromValue: 0, toValue: 0.99) This is rapid copy/paste remote pastebin code Available also with animation:
I happen to visit this question,and want to post my answer. The only thing,you need to do is use an CAShapeLayer to set Mask [graintLayer setMask:shapeLayer] I wrote an simple library about how to build Circle Graint Layer This library is here Here is the screenshot
If your backgroundLayer should just act as an mask for the gradient layer, use gradient.mask = backgroundLayer and add the gradient to the view's layer backgroundView.layer.addSublayer(gradient) otherwise create a new CAShapeLayer to act as maskLayer for the gradient
Here is Leo's library converted to Swift 3. Thanks to Alessandro Ornano's Swift 2.3 translation. Code: import UIKit class WCGraintCircleLayer: CALayer { override init () { super.init() } convenience init(bounds:CGRect,position:CGPoint,fromColor:UIColor,toColor:UIColor,linewidth:CGFloat,toValue:CGFloat) { self.init() self.bounds = bounds self.position = position let colors : [UIColor] = self.graintFromColor(fromColor: fromColor, toColor:toColor, count:4) for i in 0..<colors.count-1 { let graint = CAGradientLayer() graint.bounds = CGRect(x: 0, y: 0, width: bounds.width/2, height: bounds.height/2) let valuePoint = self.positionArrayWithMainBounds(bounds: self.bounds)[i] graint.position = valuePoint print("iesimo graint position: \(graint.position)") let fromColor = colors[i] let toColor = colors[i+1] let colors : [CGColor] = [fromColor.cgColor,toColor.cgColor] let stopOne: CGFloat = 0.0 let stopTwo: CGFloat = 1.0 let locations : [CGFloat] = [stopOne,stopTwo] graint.colors = colors graint.locations = locations as [NSNumber]? graint.startPoint = self.startPoints()[i] graint.endPoint = self.endPoints()[i] self.addSublayer(graint) //Set mask let shapelayer = CAShapeLayer() let rect = CGRect(x: 0, y: 0, width: bounds.width - 2 * linewidth, height: bounds.height - 2 * linewidth) shapelayer.bounds = rect shapelayer.position = CGPoint(x: bounds.width/2, y: bounds.height/2) shapelayer.strokeColor = UIColor.blue.cgColor shapelayer.fillColor = UIColor.clear.cgColor shapelayer.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.width/2).cgPath shapelayer.lineWidth = linewidth shapelayer.lineCap = kCALineCapRound shapelayer.strokeStart = 0.010 let finalValue = (toValue*0.99) shapelayer.strokeEnd = finalValue//0.99; self.mask = shapelayer } } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func layerWithWithBounds(bounds:CGRect, position:CGPoint, fromColor:UIColor, toColor:UIColor, linewidth : CGFloat,toValue:CGFloat) -> WCGraintCircleLayer { let layer = WCGraintCircleLayer(bounds: bounds,position: position,fromColor:fromColor, toColor: toColor,linewidth: linewidth,toValue:toValue ) return layer } func graintFromColor(fromColor:UIColor, toColor:UIColor, count:Int) -> [UIColor]{ var fromR:CGFloat = 0.0,fromG:CGFloat = 0.0,fromB:CGFloat = 0.0,fromAlpha:CGFloat = 0.0 fromColor.getRed(&fromR,green: &fromG,blue: &fromB,alpha: &fromAlpha) var toR:CGFloat = 0.0,toG:CGFloat = 0.0,toB:CGFloat = 0.0,toAlpha:CGFloat = 0.0 toColor.getRed(&toR,green: &toG,blue: &toB,alpha: &toAlpha) var result : [UIColor]! = [UIColor]() for i in 0...count { let oneR:CGFloat = fromR + (toR - fromR)/CGFloat(count) * CGFloat(i) let oneG : CGFloat = fromG + (toG - fromG)/CGFloat(count) * CGFloat(i) let oneB : CGFloat = fromB + (toB - fromB)/CGFloat(count) * CGFloat(i) let oneAlpha : CGFloat = fromAlpha + (toAlpha - fromAlpha)/CGFloat(count) * CGFloat(i) let oneColor = UIColor.init(red: oneR, green: oneG, blue: oneB, alpha: oneAlpha) result.append(oneColor) print(oneColor) } return result } func positionArrayWithMainBounds(bounds:CGRect) -> [CGPoint]{ let first = CGPoint(x: (bounds.width/4)*3, y: (bounds.height/4)*1) let second = CGPoint(x: (bounds.width/4)*3, y: (bounds.height/4)*3) let third = CGPoint(x: (bounds.width/4)*1, y: (bounds.height/4)*3) let fourth = CGPoint(x: (bounds.width/4)*1, y: (bounds.height/4)*1) print([first,second,third,fourth]) return [first,second,third,fourth] } func startPoints() -> [CGPoint] { return [CGPoint(x: 0, y: 0),CGPoint(x: 1, y: 0),CGPoint(x: 1, y: 1),CGPoint(x: 0, y: 1)] } func endPoints() -> [CGPoint] { return [CGPoint(x: 1, y: 1),CGPoint(x: 0, y: 1),CGPoint(x: 0, y: 0),CGPoint(x: 1, y: 0)] } func midColorWithFromColor(fromColor:UIColor, toColor:UIColor, progress:CGFloat) -> UIColor { var fromR:CGFloat = 0.0,fromG:CGFloat = 0.0,fromB:CGFloat = 0.0,fromAlpha:CGFloat = 0.0 fromColor.getRed(&fromR,green: &fromG,blue: &fromB,alpha: &fromAlpha) var toR:CGFloat = 0.0,toG:CGFloat = 0.0,toB:CGFloat = 0.0,toAlpha:CGFloat = 0.0 toColor.getRed(&toR,green: &toG,blue: &toB,alpha: &toAlpha) let oneR = fromR + (toR - fromR) * progress let oneG = fromG + (toG - fromG) * progress let oneB = fromB + (toB - fromB) * progress let oneAlpha = fromAlpha + (toAlpha - fromAlpha) * progress let oneColor = UIColor.init(red: oneR, green: oneG, blue: oneB, alpha: oneAlpha) return oneColor } // This is what you call if you want to draw a full circle. func animateCircle(duration: TimeInterval) { animateCircleTo(duration: duration, fromValue: 0.010, toValue: 0.99) } // This is what you call to draw a partial circle. func animateCircleTo(duration: TimeInterval, fromValue: CGFloat, toValue: CGFloat){ // We want to animate the strokeEnd property of the circleLayer let animation = CABasicAnimation(keyPath: "strokeEnd") animation.isRemovedOnCompletion = true // Set the animation duration appropriately animation.duration = duration // Animate from 0.010 (no circle) to 0.99 (full circle) animation.fromValue = 0.010 animation.toValue = toValue // Do an easeout. Don't know how to do a spring instead //animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) // Set the circleLayer's strokeEnd property to 0.99 now so that it's the // right value when the animation ends. let circleMask = self.mask as! CAShapeLayer circleMask.strokeEnd = toValue // Do the actual animation circleMask.removeAllAnimations() circleMask.add(animation, forKey: "animateCircle") } } How to use: let gradientRingLayer = WCGraintCircleLayer(bounds: CGRect(x: 0, y: 0, width: 150, height: 150), position:CGPoint(x: 200, y: 300),fromColor:UIColor.blue, toColor:UIColor.white, linewidth:4.0, toValue:0) self.view.layer.addSublayer(gradientRingLayer) let duration = 3.0 gradientRingLayer.animateCircleTo(duration: duration, fromValue: 0, toValue: 0.99)
I found a simpler answer without using mulitple layers, blending multiple layers may impact animation performance, generally should be avoided as much as possible. override func draw(_ rect: CGRect) { let lineWidth: CGFloat = CGFloat(2) let strokeColor: UIColor = UIColor.black let startAngle: CGFloat = 0 let maxAngle: CGFloat = CGFloat(Double.pi * 2) let lineCapStyle: CGLineCap = .round let gradations = 255 let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2) let radius = (min(bounds.size.width, bounds.size.height) - lineWidth) / 2 for i in 1 ... gradations { let percent0 = CGFloat(i - 1) / CGFloat(gradations) let percent1 = CGFloat(i) / CGFloat(gradations) let angle0 = startAngle + (maxAngle - startAngle) * percent0 let angle1 = startAngle + (maxAngle - startAngle) * percent1 let context = UIGraphicsGetCurrentContext()! context.setLineWidth(lineWidth) context.setLineCap(lineCapStyle) let path = CGMutablePath() path.addArc(center: center, radius: radius + lineWidth / 2, startAngle: angle0, endAngle: angle1, clockwise: true) path.addArc(center: center, radius: radius - lineWidth / 2, startAngle: angle1, endAngle: angle0, clockwise: false) path.closeSubpath() let colors = [strokeColor.withAlphaComponent(percent0).cgColor, strokeColor.withAlphaComponent(percent1).cgColor] let colorSpace = CGColorSpaceCreateDeviceRGB() let colorLocations: [CGFloat] = [0.0, 1.0] let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations)! let startPoint = CGPoint.init(x: center.x + cos(angle0) * radius, y: center.y + sin(angle0) * radius) let endPoint = CGPoint.init(x: center.x + cos(angle1) * radius, y: center.y + sin(angle1) * radius) context.saveGState() context.addPath(path) context.clip() context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: []) context.restoreGState() } }
As of iOS 12, you can use kCAGradientLayerConic as the type for your gradient layer: // Conical gradient layer CAGradientLayer *gradientLayer = [CAGradientLayer layer]; gradientLayer.frame = self.clockView.bounds; gradientLayer.type = kCAGradientLayerConic; gradientLayer.startPoint = CGPointMake(0.5, 0.5); gradientLayer.endPoint = CGPointMake(1, 1.0); gradientLayer.colors = #[(__bridge id)[UIColor colorWithRed:1.0/255.0 green:25.0/255.0 blue:147.0/255.0 alpha:1.0].CGColor, (__bridge id)[UIColor colorWithRed:250.0/255.0 green:226.0/255.0 blue:50.0/255.0 alpha:1.0].CGColor, (__bridge id)[UIColor colorWithRed:250.0/255.0 green:226.0/255.0 blue:50.0/255.0 alpha:1.0].CGColor, (__bridge id)[UIColor colorWithRed:1.0/255.0 green:25.0/255.0 blue:147.0/255.0 alpha:1.0].CGColor]; gradientLayer.locations = #[[NSNumber numberWithFloat:0.0f], [NSNumber numberWithFloat:0.4], [NSNumber numberWithFloat:0.5], [NSNumber numberWithFloat:0.6], [NSNumber numberWithFloat:1.0f]]; // Circular mask for gradient layer CAShapeLayer *maskLayer = [CAShapeLayer layer]; maskLayer.frame = gradientLayer.bounds; CGMutablePathRef circlePath = CGPathCreateMutable(); CGPathAddEllipseInRect(circlePath, NULL, CGRectInset(self.view.layer.bounds, 10.0, 10.0)); maskLayer.path = circlePath; gradientLayer.mask = maskLayer; [self.view.layer addSublayer:gradientLayer]; CGPathRelease(circlePath); // Place a second layer over the gradient layer... CALayer *circleLayer = [CALayer layer]; circleLayer.frame = gradientLayer.bounds; [circleLayer setBackgroundColor:[UIColor darkGrayColor].CGColor]; // ...and mask it with a smaller circle CAShapeLayer *maskLayer2 = [CAShapeLayer layer]; maskLayer2.frame = circleLayer.bounds; CGMutablePathRef circlePath2 = CGPathCreateMutable(); CGPathAddEllipseInRect(circlePath2, NULL, CGRectInset(self.view.layer.bounds, 60.0, 60.0)); maskLayer2.path = circlePath2; CGPathRelease(circlePath2); circleLayer.mask = maskLayer2; [self.view.layer insertSublayer:circleLayer atIndex:1];