Search code examples
iosswift3calayercabasicanimation

CALayer resizing back to original size after transform animation


I'm trying to implement two consecutive transformation animations. When the first animation ends, the second animation is called through the completion handler. Because this is a transformation animation, my issue is that when the first animation finishes, the layer resizes back to the original size, and then the second animation begins. I'd like for the second animation to begin with the new layer size after the first transformation animation. This post Objective-C - CABasicAnimation applying changes after animation? says I have to resize/transform the layer before beginning the first animation, so that when the first animation ends, the layer is actually the new size. I've tried to do that by changing the bounds or actually applying the transform to the layer but its still not working.

override func viewDidAppear(_ animated: Bool) {

    buildBar()   

}

func buildBar(){

    progressBar1.bounds = CGRect(x: 0, y: 0, width: 20, height: 5)
    progressBar1.position = CGPoint(x: 0, y: 600)
    progressBar1.backgroundColor = UIColor.white.cgColor
    view.layer.addSublayer(progressBar1)
    extendBar1()

}


func extendBar1(){

     CATransaction.begin()

     let transform1 = CATransform3DMakeScale(10, 1, 1)
     let anim = CABasicAnimation(keyPath: "transform")
     // self.progressBar1.bounds = CGRect(x: 0, y: 0, width: 200, height: 5)
     // self.progressBar1.transform = transform1
     anim.isRemovedOnCompletion = false
     anim.fillMode = kCAFillModeForwards
     anim.toValue = NSValue(caTransform3D:transform1)
     anim.duration = 5.00

     CATransaction.setCompletionBlock {

         self.extendBar2()
     }

     progressBar1.add(anim, forKey: "transform")
     CATransaction.commit()
}

func extendBar2(){

     let transform1 = CATransform3DMakeScale(2, 1, 1)
     let anim = CABasicAnimation(keyPath: "transform")
     anim.isRemovedOnCompletion = false
     anim.fillMode = kCAFillModeForwards
     anim.toValue = NSValue(caTransform3D:transform1)
     anim.duration = 5.00
     progressBar1.add(anim, forKey: "transform")

}

Solution

  • Because you are modifying the transform property of the layer in both animation, it will be easier here to use CAKeyframeAnimation, which will handle the chaining of the animations for you.

    func extendBar(){
         let transform1 = CATransform3DMakeScale(10, 1, 1)
         let transform2 = CATransform3DMakeScale(2, 1, 1)
    
         let anim = CAKeyframeAnimation()
         anim.keyPath = "transform"
         anim.values = [progressBar1.transform, transform1, transform2] // the stages of the animation
         anim.keyTimes = [0, 0.5, 1] // when they occurs, 0 being the very begining, 1 the end
         anim.duration = 10.00
    
         progressBar1.add(anim, forKey: "transform")
         progressBar1.transform = transform2 // we set the transform property to the final animation's value
    }
    

    A word about the content of values and keyTimes:

    • We set the first value to be the current transform of progressBar1. This will make sure we start in the current layer's state.
    • In keyTimes, we say that at the begining, the first value in the values array should be used. We then say at animation's half time, the layer should be transformed in values second value. Hence, the animation between the initial state and the second one will occurs during that time. Then, between 0.5 to 1, we'll go from tranform1 to transform2.

    You can read more about animations in this very nice article from objc.io.


    If you really need two distinct animations (because maybe you want to add subviews to progressBar1 in between, here's the code that will do it

    func extendBar1() {
        CATransaction.begin()
        CATransaction.setCompletionBlock {
            print("side effects")
            extendBar2()
        }
        let transform1 = CATransform3DMakeScale(5, 1, 1)
        let anim = CABasicAnimation(keyPath: "transform")
    
        anim.fromValue = progressBar1.transform
        anim.toValue = transform1
        anim.duration = 2.00
        progressBar1.add(anim, forKey: "transform")
        CATransaction.setDisableActions(true)
        progressBar1.transform = transform1
        CATransaction.commit()
    }
    
    func extendBar2() {
        CATransaction.begin()
        let transform2 = CATransform3DMakeScale(2, 1, 1)
        let anim = CABasicAnimation(keyPath: "transform")
    
        anim.fromValue = progressBar1.transform
        anim.toValue = transform2
        anim.duration = 2.00
        progressBar1.add(anim, forKey: "transform")
        CATransaction.setDisableActions(true)
        progressBar1.transform = transform2
        CATransaction.commit()
    }
    

    What happens here? Basically, we setup a first "normal animation". So we create the animation, that will modify the presentation layer and set the actual layer to the final first animation's transform.

    Then, when the first animation completes, we call extendBar2 which will in turn queue a normal animation.

    You also want to call CATransaction.setDisableActions(true) before explicitly updating the transform, otherwise, core animation will create an implicit one, which will override the one created just before.