web-dev-qa-db-fra.com

Créer une animation de chargement de cercle tournant

J'essaie de créer une animation de chargeur de cercle tournant comme dans le projet Android suivant ( https://github.com/pedant/sweet-alert-dialog ).

 enter image description here

Je n'ai pas besoin de l'intégralité de la boîte de dialogue contextuelle - seulement de la partie tournante. Il change de couleur et tourne indéfiniment (jusqu'à ce que je décide de le rejeter).

Je suis un peu nouveau chez Swift et je n’ai jamais été du genre à faire des animations. Voici ce que j'ai jusqu'à présent (code trouvé dans un projet similaire pour iOS):

La configuration des calques:

    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")

mais ce que j’ai n’est pas proche et je ne sais pas du tout où aller. Ce que je fais, c’est changer une valeur et courir en constatant les conséquences, mais j’ai le sentiment d’être perdu.

Comment puis-je réaliser une telle animation comme dans l'exemple?

11
developer82

Je n'ai pas analysé de près les paramètres exacts de l'animation, mais cela me semble bien:

 spinner view

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)
    }

}
55
rob mayoff

Essayez très simplement mes trois écrans de chargeur personnalisés:

Écrire ci-dessous le code dans le fichier Viewcontoller.Swift

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()
}}

Ajoutez maintenant 3 fichiers hérités avec UiView nommé> SignView, TestView et TestView1

Code pour le fichier SignView.Swift

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 pour le fichier TestView:

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)
}

}

Et code pour le fichier TestView1.Swift

classe 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)

    }



}

}

Maintenant, lancez le code pour vérifier le chargeur d'animation. Commenter/ne commenter aucune autre méthode de chargement dans la méthode viewDidAppear de viewcontroller.

Prendre plaisir!!

1
Robin Khurana

Si quelqu'un recherche une version Objective C de la solution @rob mayoff

dans SpinnerView.h

#import <UIKit/UIKit.h>

IB_DESIGNABLE
@interface SpinnerView : UIView

@end

dans 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
0
Waseem Khan