Home > OS >  How to animate the appearing of a CALayer using CATransaction?
How to animate the appearing of a CALayer using CATransaction?

Time:10-05

I want to animate a CALAyer so that it looks like it appears out of nowhere. I thought I could first set the transform of the layer to "scale to x: 0, y: 0". Then do an animation to set the transform to "scale to x: 1, y: 1", which is the identity. I thought this would make the layer appear to scale up from nothing. However, when I tried to implement this, the layer appeared immediately without animation.

MCVE:

class MyView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    func commonInit() {
        backgroundColor = .clear
        clipsToBounds = false
        setupLayers()
    }
    
    var squareLayer: CAShapeLayer!
    
    private func setupLayers() {
        layer.sublayers?.forEach { $0.removeFromSuperlayer() }
        squareLayer = CAShapeLayer()
        layer.addSublayer(squareLayer)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        drawLayers()
    }
    
    func drawLayers() {
        squareLayer.fillColor = UIColor.red.cgColor
        squareLayer.path = UIBezierPath(rect: bounds).cgPath
        squareLayer.frame = bounds
    }
}

Then in my VC:

@IBAction func click() {
    myView = MyView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
    view.addSubview(myView)
    myView.drawLayers()
    myView.squareLayer.setAffineTransform(.init(scaleX: 0, y: 0))
    CATransaction.begin()
    CATransaction.setAnimationDuration(1)
    self.myView.squareLayer.setAffineTransform(.init(scaleX: 1, y: 1))
    CATransaction.commit()
}

I thought this was because the first setAffineTransform call is also animated (I didn't know exactly why that would have an effect, but I suspected that might have an effect), so I tried moving the second call into the completion block:

@IBAction func click() {
    myView = MyView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
    view.addSubview(myView)
    myView.drawLayers()
    CATransaction.begin()
    CATransaction.setCompletionBlock {
        CATransaction.begin()
        CATransaction.setAnimationDuration(1)
        self.myView.squareLayer.setAffineTransform(.init(scaleX: 1, y: 1))
        CATransaction.commit()
    }
    myView.squareLayer.setAffineTransform(.init(scaleX: 0, y: 0))
    CATransaction.commit()
}

However, the output is the same. On the other hand, if I wait for a bit after the first setAffineTransform call, then it works:

@IBAction func click() {
    myView = MyView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
    view.addSubview(myView)
    myView.drawLayers()
    myView.squareLayer.setAffineTransform(.init(scaleX: 0, y: 0))
    DispatchQueue.main.asyncAfter(deadline: .now()   0.1) {
        CATransaction.begin()
        CATransaction.setAnimationDuration(1)
        self.myView.squareLayer.setAffineTransform(.init(scaleX: 1, y: 1))
        CATransaction.commit()
    }
}

I don't think this is how I'm supposed to solve this problem though. Presumably, during that time, some lifecycle method gets called (I don't know which), and what I'm supposed to do is to do the second setAffineTransform call after that lifecycle method... (?)

I know I can use CABasicAnimation for this instead, but I don't like how I need to use a delegate to detect the end of animations. This makes many things very cumbersome for me. Since delaying by 0.1 seconds worked, this can't be impossible with CATransactions, right?

How can I make the CALayer appear using an animation?

CodePudding user response:

On the other hand, if I wait for a bit

That's correct. A layer must be part of the render tree in order to be animated, so you cannot add a layer and animate it as part of the same transaction. Thus your async call waits for the current transaction to commit and the next one to start before attaching the animation, and is in fact a totally correct solution.

Another possibility is to call CATransaction.flush() after adding the layer; this effectively commits the current transaction early.

  • Related