Search code examples
swiftxcodeuikitcore-animationibdesignable

Preview CAShapeLayer using IBDesignable and IBInspectable


I try to create a view that includes a shape layer, which can be previewed inside Xcode. I somehow do not manage to get it working completely although most of it works. Specifically I cannot figure out how I can set the shape layer's stroke property since it needs to be set before the layer is added otherwise no strokes are rendered. But if I set it before then the inspectable property does not work anymore. Here is how far I came:

import Foundation
import UIKit

@IBDesignable
class CustomView: UIView
{

    @IBInspectable var layerBackgroundColor: UIColor? {
        didSet {
            if let actualColor = layerBackgroundColor {
                layer.backgroundColor = actualColor.cgColor
            } else {
                layerBackgroundColor = nil
            }
        }
    }

    @IBInspectable var strokeColor: UIColor? = UIColor.red {
        didSet {
            if let actualColor = strokeColor {
                self.shapeLayer.strokeColor = actualColor.cgColor
            } else {
                shapeLayer.strokeColor = nil
            }
        }
    }

    private var shapeLayer: CAShapeLayer

    override func prepareForInterfaceBuilder()
    {
        let width = self.bounds.width
        let height = self.bounds.height

        shapeLayer = CAShapeLayer()
        shapeLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
        let path = CGMutablePath()
        let middle = width/2
        path.move(to:CGPoint(x:middle, y:20))

        path.addLine(to:CGPoint(x:middle, y:100))

        layerBackgroundColor = UIColor.gray
        // If this is not set no stroke will be rendered!
        strokeColor = UIColor.red
        shapeLayer.path = path
        shapeLayer.fillColor = nil
        shapeLayer.lineJoin = kCALineJoinRound
        shapeLayer.lineCap = kCALineCapRound
        shapeLayer.lineWidth = 5
        layer.addSublayer(shapeLayer)

    }


    override init(frame: CGRect)
    {
        shapeLayer = CAShapeLayer()
        super.init(frame:frame)
    }

    required init?(coder aDecoder: NSCoder)
    {
        fatalError("init(coder:) has not been implemented")
    }
}

Solution

  • You are creating a new shapeLayer inside prepareForInterfaceBuilder, so you need to set the color properties of the shapeLayer inside that method. The inspectable variables are being assigned after initialization but before prepareForInterfaceBuilder, so creating a new CAShapeLayer inside prepareForInterfaceBuilder is trashing your previous assignments. I assume in practice you are going to be doing the custom drawing for your class in another method (since this method is never called outside of IB).

    Anyway, the following code should fix your problems with IB, I made four changes, all commented....

    import Foundation
    import UIKit
    
    @IBDesignable
    class CustomView: UIView
    {
        @IBInspectable var layerBackgroundColor: UIColor? {
            didSet {
                if let actualColor = layerBackgroundColor {
                    layer.backgroundColor = actualColor.cgColor
                } else {
                    layerBackgroundColor = nil
                }
            }
        }
    
        @IBInspectable var strokeColor: UIColor? {//1 -  Maybe you want to comment out assignment to UIColor.red:   = UIColor.red {
            didSet {
                if let actualColor = strokeColor {
                    self.shapeLayer.strokeColor = actualColor.cgColor
                } else {
                    shapeLayer.strokeColor = nil
                }
            }
        }
    
        private var shapeLayer: CAShapeLayer
    
        override func prepareForInterfaceBuilder()
        {
            let width = self.bounds.width
            let height = self.bounds.height
    
            shapeLayer = CAShapeLayer()
            shapeLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
            let path = CGMutablePath()
            let middle = width/2
            path.move(to:CGPoint(x:middle, y:20))
    
            path.addLine(to:CGPoint(x:middle, y:100))
    
            //2 - you probably also want to set the shapeLayer's background color to the class variable, not hardcode the class variable
            //layerBackgroundColor = UIColor.gray
            shapeLayer.backgroundColor = layerBackgroundColor?.cgColor
    
    
            // If this is not set no stroke will be rendered!
            //3 Comment the hardcoding line out
            //strokeColor = UIColor.red
            //4 - assign the shapeLayer's stroke color
            shapeLayer.strokeColor = strokeColor?.cgColor
    
            shapeLayer.path = path
            shapeLayer.fillColor = nil
            shapeLayer.lineJoin = kCALineJoinRound
            shapeLayer.lineCap = kCALineCapRound
            shapeLayer.lineWidth = 5
            layer.addSublayer(shapeLayer)
    
        }
    
    
        override init(frame: CGRect)
        {
            shapeLayer = CAShapeLayer()
            super.init(frame:frame)
        }
    
        required init?(coder aDecoder: NSCoder)
        {
            fatalError("init(coder:) has not been implemented")
        }
    }