iosswiftuikitcore-animationcashapelayer

Changing a CAShapeLayer's bounds size height doesn't change the actual size, it changes it's position


A CAShapeLayer is added as a sublayer of a view in a certain position:

// Use Bezier Path to create a rectangle shape
let barRect = CGRect(x: 0, y: 0, width: 10, height: 100)
let barPath = UIBezierPath(rect: barRect)
let bar = CAShapeLayer()
bar.path = barPath.cgPath
bar.lineWidth = 0.5
// Place anchor on lower left corner of the rectangle
bar.anchorPoint = CGPoint(x: 0, y: 1)
bar.bounds = CGRect(origin: CGPoint.zero, size: barRect.size)
// Position bar in view
bar.position = CGPoint(x: 0, y: 200)
layer.addSublayer(bar)

Later on, an action attempts to increase the height of the rectangle:

bar.bounds.size.height = 150

When this action is executed, the rectangle changes it's position in the view, but not it's height. The top of the rectangle moves to where the rectangle should be if the height is increased, but the bottom of the rectangle also moves up, maintaining the original height of the rectangle. What is the problem here? Thanks


Solution

  • Changing the layer's bounds does not change the layer's .path.

    If your goal is simple rectangles, you can use CALayer instead of CAShapeLayer with a path.

    Here's a quick example:

    class BarView: UIView {
        
        let bar1 = CAShapeLayer()
        let bar2 = CALayer()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            
            let barRect = CGRect(x: 0, y: 0, width: 10, height: 100)
            let barPath = UIBezierPath(rect: barRect)
            bar1.path = barPath.cgPath
    
            bar1.lineWidth = 0.5
            // Place anchor on lower left corner of the rectangle
            bar1.anchorPoint = CGPoint(x: 0, y: 1)
            bar1.bounds = CGRect(origin: CGPoint.zero, size: barRect.size)
            // Position bar in view
            bar1.position = CGPoint(x: 0, y: 200)
            layer.addSublayer(bar1)
            
            bar2.borderWidth = 0.5
            // Place anchor on lower left corner of the rectangle
            bar2.anchorPoint = CGPoint(x: 0, y: 1)
            bar2.bounds = CGRect(origin: CGPoint.zero, size: barRect.size)
            // Position bar in view
            bar2.position = CGPoint(x: 40, y: 200)
            layer.addSublayer(bar2)
    
            bar1.fillColor = UIColor.red.cgColor
            bar1.strokeColor = UIColor.cyan.cgColor
            
            bar2.backgroundColor = UIColor.cyan.cgColor
            bar2.borderColor = UIColor.red.cgColor
    
            self.backgroundColor = .yellow
            
        }
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            // this does not change the layer's PATH
            bar1.bounds.size.height = bar1.bounds.size.height == 150 ? 100 : 150
            bar2.bounds.size.height = bar2.bounds.size.height == 150 ? 100 : 150
        }
        
    }
    

    and a sample controller:

    class BarLayerVC: UIViewController {
        
        let someView = BarView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
            
            someView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(someView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                someView.widthAnchor.constraint(equalToConstant: 100.0),
                someView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                
                someView.heightAnchor.constraint(equalToConstant: 200.0),
                someView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
                
            ])
            
        }
        
    }
    

    Tapping the yellow "BarView" will toggle the bar bounds heights between 100 and 150 ... Red bar is your original CAShapeLayer and Cyan bar is a CALayer:

    enter image description here enter image description here


    Edit

    Here's that same BarView class, but with a 3rd (green) bar. It uses a CAShapeLayer and updates its .path in layoutSubviews():

    class BarView: UIView {
        
        let bar1 = CAShapeLayer()
        let bar2 = CALayer()
        let bar3 = CAShapeLayer()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            
            let barRect = CGRect(x: 0, y: 0, width: 10, height: 100)
            let barPath = UIBezierPath(rect: barRect)
            bar1.path = barPath.cgPath
    
            bar1.lineWidth = 0.5
            // Place anchor on lower left corner of the rectangle
            bar1.anchorPoint = CGPoint(x: 0, y: 1)
            bar1.bounds = CGRect(origin: CGPoint.zero, size: barRect.size)
            // Position bar in view
            bar1.position = CGPoint(x: 0, y: 200)
            layer.addSublayer(bar1)
            
            bar2.borderWidth = 0.5
            // Place anchor on lower left corner of the rectangle
            bar2.anchorPoint = CGPoint(x: 0, y: 1)
            bar2.bounds = CGRect(origin: CGPoint.zero, size: barRect.size)
            // Position bar in view
            bar2.position = CGPoint(x: 40, y: 200)
            layer.addSublayer(bar2)
    
            bar1.fillColor = UIColor.red.cgColor
            bar1.strokeColor = UIColor.cyan.cgColor
            
            bar2.backgroundColor = UIColor.cyan.cgColor
            bar2.borderColor = UIColor.red.cgColor
    
            bar3.fillColor = UIColor.green.cgColor
            bar3.strokeColor = UIColor.blue.cgColor
            
            bar3.lineWidth = 0.5
            // Place anchor on lower left corner of the rectangle
            bar3.anchorPoint = CGPoint(x: 0, y: 1)
            bar3.bounds = CGRect(origin: CGPoint.zero, size: barRect.size)
            // Position bar in view
            bar3.position = CGPoint(x: 80, y: 200)
            layer.addSublayer(bar3)
            
            self.backgroundColor = .yellow
            
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            
            let barRect = CGRect(x: 0, y: 0, width: 10, height: bar3.bounds.height)
            let barPath = UIBezierPath(roundedRect: barRect, cornerRadius: 4.0)
    
            bar3.path = barPath.cgPath
            
        }
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            // this does not change the layer's PATH
            bar1.bounds.size.height = bar1.bounds.size.height == 150 ? 100 : 150
            bar2.bounds.size.height = bar2.bounds.size.height == 150 ? 100 : 150
            bar3.bounds.size.height = bar3.bounds.size.height == 150 ? 100 : 150
        }
        
    }
    

    enter image description here enter image description here

    I changed the path for "bar3" to a roundedRect so we can see why we might want to use a CAShapeLayer.