Search code examples
iosswiftuikit

How to animate the shadow border of an UIView at the same time its content grows


I am working on a project which has all views embedded in a view container like this:

enter image description here

Today, I have to make one of these containers grow vertically in a animatable way when a stack view is showing its arranged subviews. So, the first problem was that the shadow border of this container is not being animated and I realized that the shadow path should be animated in another way and not just in a UIView.animate block.

I have isolated the problem in a small project:

enter image description here

As you can see, I am trying to deploy/undeploy the container view depending on the inner stackview arranged subviews. The problem is the shadow path, which I am already trying to understand how it works.

Can you help me to understand how to manage a UIView shadow layer when you need to update the height of an UIView in an animation block?

Container Shadow view code:

import UIKit

@IBDesignable final class ShadowView: UIView {
    
    @IBInspectable var cornerRadius: CGFloat = 0.0 {
        didSet {
            setNeedsLayout()
        }
    }
    @IBInspectable var shadowColor: UIColor = UIColor.darkGray {
        didSet {
            setNeedsLayout()
        }
    }
    @IBInspectable var shadowOffsetWidth: CGFloat = 0.0 {
        didSet {
            setNeedsLayout()
        }
    }
    @IBInspectable var shadowOffsetHeight: CGFloat = 1.8 {
        didSet {
            setNeedsLayout()
        }
    }
    @IBInspectable var shadowOpacity: Float = 0.30 {
        didSet {
            setNeedsLayout()
        }
    }
    @IBInspectable var shadowRadius: CGFloat = 3.0 {
        didSet {
            setNeedsLayout()
        }
    }
    @IBInspectable var isAnimatable: Bool = false
    
    private var shadowLayer: CAShapeLayer = CAShapeLayer() {
        didSet {
            setNeedsLayout()
        }
    }
    
    private var previousPat: CGPath = CGPath(rect: CGRect(x: 0, y: 0, width: 0, height: 0), transform: .none)
    
    override func layoutSubviews() {
        super.layoutSubviews()
        layer.cornerRadius = cornerRadius
        shadowLayer.path = UIBezierPath(roundedRect: bounds,
                                        cornerRadius: cornerRadius).cgPath
        shadowLayer.fillColor = backgroundColor?.cgColor
        shadowLayer.shadowColor = shadowColor.cgColor
        shadowLayer.shadowPath = shadowLayer.path
        shadowLayer.shadowOffset = CGSize(width: shadowOffsetWidth,
                                          height: shadowOffsetHeight)
        shadowLayer.shadowOpacity = shadowOpacity
        shadowLayer.shadowRadius = shadowRadius
        if isAnimatable {
            let animation = CABasicAnimation(keyPath: "shadowPath")
            animation.fromValue = previousPat
            animation.toValue = shadowLayer.path
            animation.autoreverses = false
            animation.duration = 0.8
            shadowLayer.add(animation, forKey: "pathAnimation")
        }
        layer.insertSublayer(shadowLayer, at: 0)
        previousPat = shadowLayer.path!
        
    }
}

Main ViewController code:

final class ViewController: UIViewController {

    @IBOutlet weak var stack: UIStackView!
    @IBOutlet weak var redContainerLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func animateAction(_ sender: Any) {
        UIView.animate(withDuration: 0.8) {
            self.redContainerLabel.alpha = self.redContainerLabel.alpha == 0 ? 1 : 0
            self.stack.arrangedSubviews.forEach {
                $0.isHidden.toggle()
            }
        }
    }
    
}

View structure: enter image description here


Solution

  • I was looking for an answer to exactly this problem and encountered this post here, which I believe is what you're looking for:

    Animating CALayer shadow simultaneously as UITableviewCell height animates

    The solution is a bit verbose (I'm surprised the use-case is so rare as to not prompt apple to fix(?) it) and a few years old at this point, but the most recent I could find. Ultimately I decided to work around it by fading out my shadows before resizing, but you might find it useful!

    A couple of other, older posts I've found appear to corroborated the level of effort required for this effect (again, Apple's got a weird list of things it decides are your problem), so that first post is probably your best bet.

    https://petosoft.wordpress.com/2012/10/24/shadowpath-not-animating-with-standard-uiview-animations-on-ipad/

    Smoothly rotate and change size of UIView with shadow

    UIView: How to animate CALayer frame with shadow?

    If you've managed to find a more straightforward solution, by all means let me know; in the mean-time I hope this helps.