Replacing a UIView backing layer with CAShapeLayer and ensuring the path is animated on frame change

Recently i’ve started making a lot of use of CAShapeLayers as the backing for a UIView subclass. The main reason for this is it gives me a reusable high performance background which is especially useful as I find a lot of my apps usually have similar looking views with different content.

This is all well and good providing you don’t do any animation however as soon as you start animating things go wrong.

Typically what will happen is when the UIView frame is changed in a UIView animation block the backing layer gets instantly updated resulting in it jumping to the new position. Which is not good.

Nick Lockwood has written an article here which talks about backing layers and overriding the actionForLayer:forKey: in the UIView subclass to animate the backing layers properties correctly during a UIView animation block. This isn’t convenient for example if the backing layer is used in more then one view as it will require copying the actionForLayer:forKey: to multiple places or creating a subclass of UIView which then must be subclassed every time you want to use it.

The good news is actionForLayer:forKey: actually calls actionForKey: in the backing layer and it’s in the actionForKey: method where we can intercept these calls and provide a new animation for when the path is changed.

An example layer written in swift is as follows:

class AnimatedBackingLayer: CAShapeLayer
{

    override var bounds: CGRect
    {
        didSet
        {
            if !CGRectIsEmpty(bounds)
            {
                path = UIBezierPath(roundedRect: CGRectInset(bounds, 10, 10), cornerRadius: 5).CGPath
            }
        }
    }

    override func actionForKey(event: String) -> CAAction?
    {
        if event == "path"
        {
            if let action = super.actionForKey("backgroundColor") as? CABasicAnimation
            {
                let animation = CABasicAnimation(keyPath: event)
                animation.fromValue = path
                // Copy values from existing action
                animation.autoreverses = action.autoreverses
                animation.beginTime = action.beginTime
                animation.delegate = action.delegate
                animation.duration = action.duration
                animation.fillMode = action.fillMode
                animation.repeatCount = action.repeatCount
                animation.repeatDuration = action.repeatDuration
                animation.speed = action.speed
                animation.timingFunction = action.timingFunction
                animation.timeOffset = action.timeOffset
                return animation
            }
        }
        return super.actionForKey(event)
    }

}

Leave a Comment

Your email address will not be published. Required fields are marked *