I am aware that a CALayer's shadowPath is only animatable using explicit animations, however I still cannot get this to work. I suspect that I am not passing the toValue
properly - as I understand this has to be an id
, yet the property takes a CGPathRef. Storing this in a UIBezierPath
does not seem to work. I am using the following code to test:
CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:@"shadowPath"];
theAnimation.duration = 3.0;
theAnimation.toValue = [UIBezierPath bezierPathWithRect:CGRectMake(-10.0, -10.0, 50.0, 50.0)];
[self.view.layer addAnimation:theAnimation forKey:@"animateShadowPath"];
(I am using minus values so as to ensure the shadow extends beyond a view that lies on top of it... the layer's masksToBounds
property is set to NO
).
How is animation of the shadowPath achieved?
UPDATE
Problem nearly solved. Unfortunately, the main problem was a somewhat careless error...
The mistake I made was to add the animation to the root layer of the view controller, rather than the layer I had dedicated to the shadow. Also, @pe8ter was correct in that the toValue
needs to be a CGPathRef
cast to id
(obviously when I had tried this before I still had no animation due to the wrong layer mistake). The animation works with the following code:
CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:@"shadowPath"];
theAnimation.duration = 3.0;
theAnimation.toValue = (id)[UIBezierPath bezierPathWithRect:myRect].CGPath;
[controller.shadowLayer addAnimation:theAnimation forKey:@"shadowPath"];
I appreciate this was difficult to spot from the sample code I provided. Hopefully it can still be of use to people in a similar situation though.
However, when I try and add the line
controller.shadowLayer.shadowPath = [UIBezierPath bezierPathWithRect:myRect].CGPath;
the animation stops working, and the shadow just jumps to the final position instantly. Docs say to add the animation with the same key as the property being changed so as to override the implicit animation created when setting the value of the property, however shadowPath can't generate implicit animations... so how do I get the new property to stay after the animation?
Firstly, you did not set the animation's fromValue
.
Secondly, you're correct: toValue
accepts a CGPathRef
, except it needs to be cast to id
. Do something like this:
theAnimation.toValue = (id)[UIBezierPath bezierPathWithRect:newRect].CGPath;
You'll also need to set the shadowPath
property of the layer explicitly if you want the change to remain after animation.
let cornerRadious = 10.0
//
let shadowPathFrom = UIBezierPath(roundedRect: rect1, cornerRadius: cornerRadious)
let shadowPathTo = UIBezierPath(roundedRect: rect2, cornerRadius: cornerRadious)
//
layer.masksToBounds = false
layer.shadowColor = UIColor.yellowColor().CGColor
layer.shadowOpacity = 0.6
//
let shadowAnimation = CABasicAnimation(keyPath: "shadowPath")
shadowAnimation.fromValue = shadowPathFrom.CGPath
shadowAnimation.toValue = shadowPathTo.CGPath
shadowAnimation.duration = 0.4
shadowAnimation.autoreverses = true
shadowAnimation.removedOnCompletion = true
shadowAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
layer.addAnimation(shadowAnimation, forKey: "shadowAnimation")
I've met same problem for shadow animation and find solution for persisting shadow after end of animation. You need to set final path to your shadow layer before you start animation. Reverting to initial shadow happens because CoreAnimation does not update properties of original layer, it creates copy of this layer and display animation on this copy ( check Neko1kat answer for more details ). After animation ended system removes this animated layer and return original layer, that has not updated path and your old shadow appears. Try this code:
let shadowAnimation = CABasicAnimation(keyPath: "shadowPath")
shadowAnimation.fromValue = currentShadowPath
shadowAnimation.toValue = newShadowPath
shadowAnimation.duration = 3.0
shadowLayer.shadowPath = newShadowPath
shadowLayer.add(shadowAnimation, forKey: "shadowAnimation")
Although not directly answer this question, if you just needs a view can drop shadow, you can use my class directly.
/*
Shadow.swift
Copyright © 2018, 2020-2021 BB9z
https://github.com/BB9z/iOS-Project-Template
The MIT License
https://opensource.org/licenses/MIT
*/
/**
A view drops shadow.
*/
@IBDesignable
class ShadowView: UIView {
@IBInspectable var shadowOffset: CGPoint = CGPoint(x: 0, y: 8) {
didSet { needsUpdateStyle = true }
}
@IBInspectable var shadowBlur: CGFloat = 10 {
didSet { needsUpdateStyle = true }
}
@IBInspectable var shadowSpread: CGFloat = 0 {
didSet { needsUpdateStyle = true }
}
/// Set nil can disable shadow
@IBInspectable var shadowColor: UIColor? = UIColor.black.withAlphaComponent(0.3) {
didSet { needsUpdateStyle = true }
}
@IBInspectable var cornerRadius: CGFloat {
get { layer.cornerRadius }
set { layer.cornerRadius = newValue }
}
private var needsUpdateStyle = false {
didSet {
guard needsUpdateStyle, !oldValue else { return }
DispatchQueue.main.async { [self] in
if needsUpdateStyle { updateLayerStyle() }
}
}
}
private func updateLayerStyle() {
needsUpdateStyle = false
if let color = shadowColor {
Shadow(view: self, offset: shadowOffset, blur: shadowBlur, spread: shadowSpread, color: color, cornerRadius: cornerRadius)
} else {
layer.shadowColor = nil
layer.shadowPath = nil
layer.shadowOpacity = 0
}
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
updateLayerStyle()
}
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
lastLayerSize = layer.bounds.size
if shadowColor != nil, layer.shadowOpacity == 0 {
updateLayerStyle()
}
}
private var lastLayerSize = CGSize.zero {
didSet {
if oldValue == lastLayerSize { return }
guard shadowColor != nil else { return }
updateShadowPathWithAnimationFixes(bonuds: layer.bounds)
}
}
// We needs some additional step to achieve smooth result when view resizing
private func updateShadowPathWithAnimationFixes(bonuds: CGRect) {
let rect = bonuds.insetBy(dx: shadowSpread, dy: shadowSpread)
let newShadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
if let resizeAnimation = layer.animation(forKey: "bounds.size") {
let key = #keyPath(CALayer.shadowPath)
let shadowAnimation = CABasicAnimation(keyPath: key)
shadowAnimation.duration = resizeAnimation.duration
shadowAnimation.timingFunction = resizeAnimation.timingFunction
shadowAnimation.fromValue = layer.shadowPath
shadowAnimation.toValue = newShadowPath
layer.add(shadowAnimation, forKey: key)
}
layer.shadowPath = newShadowPath
}
}
/**
Make shadow with the same effect as Sketch app.
*/
func Shadow(view: UIView?, offset: CGPoint, blur: CGFloat, spread: CGFloat, color: UIColor, cornerRadius: CGFloat = 0) { // swiftlint:disable:this identifier_name
guard let layer = view?.layer else {
return
}
layer.shadowColor = color.cgColor
layer.shadowOffset = CGSize(width: offset.x, height: offset.y)
layer.shadowRadius = blur
layer.shadowOpacity = 1
layer.cornerRadius = cornerRadius
let rect = layer.bounds.insetBy(dx: spread, dy: spread)
layer.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
}
via https://github.com/BB9z/iOS-Project-Template/blob/master/App/General/Effect/Shadow.swift
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.