Orion
Orion

Reputation: 1276

Swift: Animate layers in sequence from UIView

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

Upvotes: 0

Views: 1952

Answers (3)

Orion
Orion

Reputation: 1276

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.

Upvotes: 1

Graeme K
Graeme K

Reputation: 89

try this..

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

//enter item to be animated here

}, completion: nil)

Upvotes: 0

Gandalf
Gandalf

Reputation: 2417

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.

Upvotes: 0

Related Questions