I have a CAShapeLayer
instance with a non nil CALayer mask
. I'm trying to use the frame of that mask to clip the shape. Which works fine. But when I change the frame, I don't want it to animate the frame change. I update the frame in my view's layoutSubviews
and I've noticed something interesting:
override func layoutSubviews() {
super.layoutSubviews()
...
if let futureMask = self.futureShape.mask {
"0.future position animation \(futureMask.animation(forKey: "position"))".print()
futureMask.removeAllAnimations()
futureMask.add(CAAnimation(), forKey: "position")
futureMask.add(CAAnimation(), forKey: "bounds")
"1.future position animation \(futureMask.animation(forKey: "position"))".print()
let nowX = computeNowX()
futureMask.frame = CGRect(x: box.left + nowX, y: box.top, width: box.width - nowX, height: box.height)
"2.future position animation \(futureMask.animation(forKey: "position"))".print()
}
}
The output this produces looks like:
0.future position animations Optional(<CABasicAnimation:0x174624ac0; duration = 0.25; fillMode = backwards; timingFunction = default; keyPath = position; fromValue = NSPoint: {418.94695306710298, 14}>)
1.future position animation Optional(<CAAnimation:0x170625780; duration = 0.25>)
2.future position animation Optional(<CABasicAnimation:0x170625820; duration = 0.25; fillMode = backwards; timingFunction = default; keyPath = position; fromValue = NSPoint: {418.94695306710298, 14}>)
At the beginning it's a CABasicAnimation
, pointing at the position
keyPath, and with a 250 millisecond duration. I blow it away (via removeAllAnimations()
) and add a stub CAAnimation hoping that will do nothing. And the 1
print shows that that indeed takes place. But after I set the frame
property, it has returned to the original. It seems that just setting the frame resets these animations.
Is there no way to set the frame without having these animations take place? I want other animations in the system to keep working. I just want this particular masking layer to not animate.
Just call layer removeAllAnimations()
AFTER setting the frame.
class GradientView: UIView {
private(set) var gradientLayer: CAGradientLayer
override init(frame: CGRect) {
gradientLayer = CAGradientLayer()
super.init(frame: frame)
layer.insertSublayer(gradientLayer, at: 0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("not implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
gradientLayer.removeAllAnimations() // remove implicit animation from frame change
}
}
In the end, the only thing I've found that worked was doing the following:
override func layoutSubviews() {
super.layoutSubviews()
...
if let futureMask = self.futureShape.mask {
let nowX = computeNowX()
CATransaction.setDisableActions(true)
futureMask.frame = CGRect(x: box.left + nowX, y: box.top, width: box.width - nowX, height: box.height)
CATransaction.commit()
}
}
Basically, I just wrapped the setting of the frame with the CATransaction
calls to disable actions and forced a commit.
UPDATED
@Hlung's answer is more correct. After the setting of remove
, there are two animations that have been added (as shown by printing animationKeys()
at that point). I added the following extension method to capture the pattern:
extension CALayer {
func dontAnimate(_ closure:(CALayer) -> ()) {
closure(self)
self.removeAllAnimations()
}
}
This allows me to write code like:
self.someSubLayer.dontAnimate { layer in layer.frame = whatever }
A more powerful variant of dontAnimate
might first snapshot the current animations, run the closure
, and then removeAllAnimations()
followed by restoring the snapshotted ones.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With