0

I've added a few layers to an UIView and now wish to animate these in sequence.

I have 5 layers to animate, these are all circles. I use observable fields to get notified of the changes in the values and start to animate using the needsDisplayForKey() and actionForKey() methods.

The observable fields:

@NSManaged private var _startAngle: CGFloat

@NSManaged private var _endAngle: CGFloat

Currently all the layers animate at the same time when I change one of the observable fields for each layer. I wish to change this and wish to animate all of them in sequence in a time span of 2.5 seconds. So when one animation has finished, I wish to animate the next layer etc.

For now the only way I can know that an animation has finished is by using the animationDidStop() method. However this way I have no way of knowing of the other layers that are animating. Do you guys know what a good solution would be for this?

My layer:

class HourLayer: CAShapeLayer {

@NSManaged private var _startAngle: CGFloat

@NSManaged private var _endAngle: CGFloat

@NSManaged private var _fillColor: UIColor

@NSManaged private var _strokeWidth: CGFloat

@NSManaged private var _strokeColor: UIColor

@NSManaged private var _duration: CFTimeInterval

private var _previousEndAngle: CGFloat = 0.0
private var _previousStartAngle: CGFloat = 0.0

private let _lenghtOfArc: CGFloat = CGFloat(2 * M_PI)

internal var StartAngle: CGFloat {
    get {
        return _startAngle
    }
    set {
        _startAngle = newValue
    }
}

internal var EndAngle: CGFloat {
    get {
        return _endAngle
    }
    set {
        _endAngle = newValue
    }
}

internal var FillColor: UIColor {
    get {
        return _fillColor
    }
    set {
        _fillColor = newValue
    }
}

internal var StrokeWidth: CGFloat {
    get {
        return _strokeWidth
    }
    set {
        _strokeWidth = newValue
    }
}

internal var StrokeColor: UIColor {
    get {
        return _strokeColor
    }
    set {
        _strokeColor = newValue
    }
}

internal var DurationOfAnimation: CFTimeInterval {
    get {
        return _duration
    }
    set {
        _duration = newValue
    }
}

required override init!() {
    super.init()

    self._startAngle = 0.0
    self._endAngle = 0.0
    self._fillColor = UIColor.clearColor()
    self._strokeWidth = 4.0
    self._strokeColor = UIColor.blackColor()
    self._duration = 1.0
}

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    fatalError("required init(:coder) is missing")
}

override init!(layer: AnyObject!) {
    super.init(layer: layer)

    if layer.isKindOfClass(HourLayer) {
        var other: HourLayer = layer as HourLayer
        self._startAngle = other._startAngle
        self._endAngle = other._endAngle
        self._fillColor = other._fillColor
        self._strokeWidth = other._strokeWidth
        self._strokeColor = other._strokeColor
        self._duration = other._duration
    }
}

override var frame: CGRect {
    didSet {
        self.setNeedsLayout()
        self.setNeedsDisplay()
    }
}

override func actionForKey(event: String!) -> CAAction! {
    if event == "_startAngle" || event == "_endAngle" {
        return makeAnimationForKey(event)
    }
    return super.actionForKey(event)
}

private func makeAnimationForKey(event: String) -> CABasicAnimation {
    // We want to animate the strokeEnd property of the circleLayer
    let animation: CABasicAnimation = CABasicAnimation(keyPath: event)
    // Set the animation duration appropriately
    animation.duration = self._duration

    // When no animation has been triggered and the presentation layer does not exist yet.
    if self.presentationLayer() == nil {
        if event == "_startAngle" {
            animation.fromValue = self._previousStartAngle
            self._previousStartAngle = self._startAngle
        } else if event == "_endAngle" {
            animation.fromValue = self._previousEndAngle
            self._previousEndAngle = self._endAngle
        }
    } else {
        animation.fromValue = self.presentationLayer()!.valueForKey(event)
    }
    animation.removedOnCompletion = false
    animation.delegate = self
    // Do a linear animation (i.e. the speed of the animation stays the same)
    animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    return animation
}

override class func needsDisplayForKey(key: String) -> Bool {
    if key == "_startAngle" || key == "_endAngle" {
        return true
    }
    return super.needsDisplayForKey(key)
}

override func drawInContext(ctx: CGContext!) {
    var center: CGPoint = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2)
    var radius: CGFloat = CGFloat((CGFloat(self.bounds.width) - CGFloat(_strokeWidth)) / 2)

    // Set the stroke color
    CGContextSetStrokeColorWithColor(ctx, _strokeColor.CGColor)

    // Set the line width
    CGContextSetLineWidth(ctx, _strokeWidth)

    // Set the fill color (if you are filling the circle)
    CGContextSetFillColorWithColor(ctx, UIColor.clearColor().CGColor)

    // Draw the arc around the circle
    CGContextAddArc(ctx, center.x, center.y, radius, _startAngle, _lenghtOfArc * _endAngle, 0)

    // Draw the arc
    CGContextDrawPath(ctx, kCGPathStroke) // or kCGPathFillStroke to fill and stroke the circle

    super.drawInContext(ctx)
}

override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
    // Trigger animation for other layer by setting their values for _startAngle and _endAngle.
}
}
Orion
  • 1,258
  • 2
  • 14
  • 32

3 Answers3

1

The Apple documentation states the following: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreAnimation_guide/CreatingBasicAnimations/CreatingBasicAnimations.html

If you want to chain two animations together so that one starts when the other finishes, do not use animation notifications. Instead, use the beginTime property of your animation objects to start each one at the desired time. To chain two animations together, set the start time of the second animation to the end time of the first animation. For more information about animation and timing values, see Customizing the Timing of an Animation.

I have got some stuff working, but I do not find it the most ideal solution. I would rather think the notifications are a nicer way, but hey if Apple says so right....

Also I did have to set: animation.fillMode = kCAFillModeBackwards Otherwise the model layer would make the circles reach their final state before animating and only later would the animation trigger.

Edit

Ok so I found that using multiple layers for my animations is slow (I used multiple layers for each view, so a circle I wanted to animate and I had about six of 'em. Do that times 8 is very slow (6x8=48 animations).). It is just way faster to animate everything in one layer.

Orion
  • 1,258
  • 2
  • 14
  • 32
0

My solution is in objective-c, you need to adopt this for swift. Give it a try: -
Create an NSOperationQueue and add the animation calls to this queue. Set the setMaxConcurrentOperationCount = 1 to ensure the sequential execution.

NSOperationQueue *animQueue = [[NSOperationQueue alloc] init];  
[animQueue setMaxConcurrentOperationCount:1];

Now from function actionForKey instead of calling the animation function, add it to the operation queue

NSBlockOperation *animOperation = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakOp = animOperation;
[weakOp addExecutionBlock:^{
   [self makeAnimationForKey:event)]
}];
[animQueue addOperation:animOperation];

I have not tested this, but i think it should work.

Gandalf
  • 2,399
  • 2
  • 15
  • 19
  • Would you know if I can pass this OperationQueue from an UIView to CAShapeLayers without problems? – Orion Mar 13 '15 at 11:45
  • Can't say for sure, but from my understanding by adding the method to `OperationQueue` we are not actually passing any objects to some other receiver, we are just serialising the execution. So whether you want to use `UIView` or `CAShapeLayer`; shouldn't make any difference. Just give it a try, shouldn't take much time. – Gandalf Mar 13 '15 at 12:04
  • It does not seem to work when passing the queue to the CAShapeLayers from the UIView in which the layers are added. – Orion Mar 13 '15 at 12:42
0

try this..

    UIview.animateWithDuration(2.0, delay 2.0, options: .CurveEaseIn, animations: {

//enter item to be animated here

}, completion: nil)
Graeme K
  • 89
  • 8
  • I have been researching this and my answer given here: http://stackoverflow.com/questions/28784108/animating-strokeend-in-a-cashapelayer-using-an-animation-block-in-swift/29099102#29099102 shows that you can not animate the endAngle property with this method. – Orion Mar 17 '15 at 14:03