Search code examples
uiviewcore-animationcalayercabasicanimation

CALayer Presentation nil


I am trying to use a CABasicAnimation for the timing function with custom objects (not UIView).

I'm trying to implement @CIFilter's answer from here which is to use the CALayer's presentation layer that is animated to evaluate the timing function.

I'm doing it all in viewDidAppear, so a valid view exists, but no matter what I do, the Presentation layer is always nil.

Note that I have to add the animation to the view's layer and not the layer I've added to it for it to animate at all. And if I uncomment the lines commented out below I can see that the animation works (but only when animating the root layer). Regardless, the Presentation layer is nil.

I've looked at dozen's of tutorials and SO answers, and it seems this should just work, so I suppose I must be doing something stupid.

I am just trying to use the CoreAnimation timing functions. I have UICubicTimingParameters working, but seems like going the CA route offers much more functionality which would be nice.

import UIKit

class ViewController: UIViewController {

    override func viewDidAppear(_ animated: Bool) {
        
        super.viewDidAppear(animated)
    
        let newView = UIView(frame: view.frame)
        view.addSubview(newView)

        let evaluatorLayer = CALayer()
        evaluatorLayer.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
        evaluatorLayer.borderWidth = 8.0
        evaluatorLayer.borderColor = UIColor.purple.cgColor
        evaluatorLayer.timeOffset = 0.3
        evaluatorLayer.isHidden = true
    //  evaluatorLayer.isHidden = false
    
        newView.layer.addSublayer(evaluatorLayer)
    
        let basicAnimation = CABasicAnimation(keyPath: "bounds.origin.x")
        basicAnimation.duration = 1.0
        basicAnimation.fromValue = 0.0
        basicAnimation.toValue = 100.0
        basicAnimation.fillMode = .forwards
        basicAnimation.isRemovedOnCompletion = false
        basicAnimation.speed = 0.0
    //  basicAnimation.speed = 0.1
    
        newView.layer.add(basicAnimation, forKey: "evaluate")

        if let presentationLayer = newView.layer.presentation() {
            let evaluatedValue = presentationLayer.bounds.origin.x
            print("evaluatedValue: \(evaluatedValue)")
        }
        else {
            print(evaluatorLayer.presentation())
        }
    
    }
}

Solution

  • Not sure if your code is going to do what you expect, but...

    I think the reason .presentation() is nil is because you haven't given UIKit an opportunity to apply the animation.

    Try this:

    override func viewDidAppear(_ animated: Bool) {
        
        super.viewDidAppear(animated)
        
        let newView = UIView(frame: view.frame)
        view.addSubview(newView)
        
        let evaluatorLayer = CALayer()
        evaluatorLayer.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
        evaluatorLayer.borderWidth = 8.0
        evaluatorLayer.borderColor = UIColor.purple.cgColor
        evaluatorLayer.timeOffset = 0.3
        evaluatorLayer.isHidden = true
        //  evaluatorLayer.isHidden = false
        
        newView.layer.addSublayer(evaluatorLayer)
        
        let basicAnimation = CABasicAnimation(keyPath: "bounds.origin.x")
        basicAnimation.duration = 1.0
        basicAnimation.fromValue = 0.0
        basicAnimation.toValue = 100.0
        basicAnimation.fillMode = .forwards
        basicAnimation.isRemovedOnCompletion = false
        basicAnimation.speed = 0.0
        //  basicAnimation.speed = 0.1
        
        newView.layer.add(basicAnimation, forKey: "evaluate")
    
        DispatchQueue.main.async {
            
            if let presentationLayer = newView.layer.presentation() {
                let evaluatedValue = presentationLayer.bounds.origin.x
                print("async evaluatedValue: \(evaluatedValue)")
            }
            else {
                print("async", evaluatorLayer.presentation())
            }
            
        }
    
        if let presentationLayer = newView.layer.presentation() {
            let evaluatedValue = presentationLayer.bounds.origin.x
            print("immediate evaluatedValue: \(evaluatedValue)")
        }
        else {
            print("immediate", evaluatorLayer.presentation())
        }
        
    }
    

    My debug output is:

    immediate nil
    async evaluatedValue: 0.0
    

    Edit

    I'm still not sure what your goal is, but give this a try...

    override func viewDidAppear(_ animated: Bool) {
        
        super.viewDidAppear(animated)
        
        let newView = UIView(frame: view.frame)
        view.addSubview(newView)
        
        let evaluatorLayer = CALayer()
        evaluatorLayer.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
        evaluatorLayer.borderWidth = 8.0
        evaluatorLayer.borderColor = UIColor.purple.cgColor
        evaluatorLayer.isHidden = true
        //evaluatorLayer.isHidden = false
        
        newView.layer.addSublayer(evaluatorLayer)
        
        let basicAnimation = CABasicAnimation(keyPath: "bounds.origin.x")
        basicAnimation.duration = 1.0
        basicAnimation.fromValue = 0.0
        basicAnimation.toValue = 100.0
        basicAnimation.fillMode = .forwards
        basicAnimation.isRemovedOnCompletion = false
        basicAnimation.speed = 0.0
        //basicAnimation.speed = 1.0
    
        // set timeOffset on the animation, not on the layer itself
        basicAnimation.timeOffset = 0.3
    
        // add animation to evaluatorLayer
        evaluatorLayer.add(basicAnimation, forKey: "evaluate")
        
        DispatchQueue.main.async {
            
            // get presentation layer of evaluatorLayer
            if let presentationLayer = evaluatorLayer.presentation() {
                let evaluatedValue = presentationLayer.bounds.origin.x
                print("async evaluatedValue: \(evaluatedValue)")
            }
            else {
                print("async", evaluatorLayer.presentation())
            }
            
        }
    
    }
    

    In this example, we apply the .timeOffset on the animation, not on the layer. And, we add the animation to the evaluatorLayer, not to the newView.layer.

    Output (for my quick test):

    async evaluatedValue: 30.000001192092896