0

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())
        }
    
    }
}
Jeshua Lacock
  • 5,730
  • 1
  • 28
  • 58

1 Answers1

0

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
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • I was expecting it to work because that is what the answer I linked to says to do and has been upvoted. But, yeah, I get a presentation layer now but `evaluatedValue` is 0.0 even if I set the timeOffset. So something is still not right, but you did answer the question of why presentation is nil, so I accepted it. – Jeshua Lacock May 12 '21 at 04:21
  • @JeshuaLacock - see the **Edit** to my answer. – DonMag May 12 '21 at 13:02
  • Thank you. That actually worked - once. But then 0 every time. Hmm, kind of funky to have to use .async for what I want to use it for anyways, so think I'll use what I have now. Darn shame  doesn't expose CATimer functions for use on our own objects. – Jeshua Lacock May 13 '21 at 06:52