创建旋转圆圈加载动画

时间:2015-10-31 16:23:39

标签: ios swift animation

我正在尝试创建一个旋转圈加载器动画,就像在以下Android项目中一样(https://github.com/pedant/sweet-alert-dialog)。

enter image description here

我不需要整个弹出对话框 - 只需要旋转部分。它会无限地改变颜色和旋转(直到我选择解除它)。

我是一个快速的新手,我从来没有做过动画的那种。这是我到目前为止(在iOS的类似项目中找到代码):

图层设置:

    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)

动画:

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

但我所拥有的并不近,我不知道从哪里开始。我正在做的是改变价值并看看它是如何影响的,但我觉得我在这里迷失了。

如何在示例中实现此类动画?

3 个答案:

答案 0 :(得分:63)

我没有仔细分析动画的确切参数,但这对我来说很好看:

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

}

答案 1 :(得分:1)

尝试我的三个自定义Loader屏幕非常简单:

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

现在使用名为>的UiView添加3个文件继承SignView,TestView和TestView1

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

}

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

}

TestView1.swift文件的代码

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

    }



}

}

现在运行代码以检查动画加载器。在viewcontroller的viewDidAppear方法中注释/取消注释其他2个加载器方法。

享受!!

答案 2 :(得分:0)

如果有人正在寻找@rob mayoff解决方案的Objective C版本

SpinnerView.h中的

#import <UIKit/UIKit.h>

IB_DESIGNABLE
@interface SpinnerView : UIView

@end
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