2
votes

I am trying to create a custom view by extending the class UIView which can show a circle at the centre of the custom view. In order to add custom drawing, I override draw(_ rect: CGRect) method as below.

    public override func draw(_ rect: CGRect) {
        super.draw(rect)
        let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
        let minLength = bounds.width <= bounds.height ? bounds.width : bounds.height
        let circlePath = UIBezierPath (arcCenter: center, radius: CGFloat(minLength  / 2), startAngle: 0, endAngle: CGFloat(Float(360).degreesToRadians) , clockwise: true)

        let shapeLayer = CAShapeLayer()
        shapeLayer.path = circlePath.cgPath
        shapeLayer.fillColor = circleColor.cgColor
        layer.addSublayer(shapeLayer)
    }

But when I try to add the same custom component in storyboard and added a breakpoint in draw method, it was not getting called. To my curiosity, I added a breakpoint to init?(coder aDecoder: NSCoder) method which was getting called. Also when I try to override the method as below, it worked fine.

required public init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    drawCircle()
}

fileprivate func drawCircle() {
    let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
    let minLength = bounds.width <= bounds.height ? bounds.width : bounds.height
    let circlePath = UIBezierPath (arcCenter: center, radius: CGFloat(minLength  / 2), startAngle: 0, endAngle: CGFloat(Float(360).degreesToRadians) , clockwise: true)

    let shapeLayer = CAShapeLayer()
    shapeLayer.path = circlePath.cgPath
    shapeLayer.fillColor = circleColor.cgColor

    layer.addSublayer(shapeLayer)
}

The issue with approach is I have a custom property circleColor

@IBInspectable
public var circleColor : UIColor = UIColor.gray

which is not yet set when init method is called and it always draws a gray circle.

Please help to get me know the reason why draw method is not being called.

Thanks in advance

1
By the way, you can use let minLength = min(bounds.width, bounds.height) and if degressToRadians is implemented correctly, it should be enough to use CGFloat(360).degreesToRadiansSulthan
Note that adding a sublayer in draw is not a good idea. Sublayers should be added only once, not every time you rerender. Also note that you would have to update the frame of the layer when needed. Also note that you should always implement all designated initializers, not just init(coder:), init(frame:) must be implemented too.Sulthan
@Sulthan point noted. will change the same. thanks for the suggestion. Still, my question is, what is the minimum criteria to call the overridden draw method. am I missing something?Suneet Agrawal
The overriden method should be called everytime the component needs to be rerendered, that is, when setNeedsDisplay() has been called. It should be always called at least once. It's strange that it is not called at all.Sulthan
Of course, the view has to be added to a superview and have a nonzero size.Sulthan

1 Answers

4
votes

I am not sure where your root mistake lies but you should not override draw(:) just to add a layer. That would mean a new layer will be added again every time the component rerenders. That's not what you want.

You should also make sure the layer color is updated when you change the color property. To do that, just save the reference to the layer:

private let shapeLayer = CAShapeLayer()

@IBInspectable
public var circleColor : UIColor = UIColor.gray  {
   didSet {
      shapeLayer.fillColor = circleColor.cgColor
   }
}

public override init(frame: CGRect = .zero) {
    super.init(frame: frame)
    updateLayer()
    layer.addSublayer(shapeLayer)
}

required public init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    updateLayer()
    layer.addSublayer(shapeLayer)
}

fileprivate func updateLayer() {
    let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
    let minLength = min(bounds.width, bounds.height)
    let circlePath = UIBezierPath(arcCenter: center, radius: minLength / 2, startAngle: 0, endAngle: CGFloat(Float(360).degreesToRadians), clockwise: true)

    shapeLayer.path = circlePath.cgPath
    shapeLayer.fillColor = circleColor.cgColor
}

override func layoutSubviews() {
    super.layoutSubviews()
    // update layer position
    updateLayer()
}

If you really want to override draw(rect:), you don't need to create a layer or override initializers at all:

@IBInspectable
public var circleColor : UIColor = UIColor.gray  {
   didSet {
      setNeedsDisplay()
   }
}

public override func draw(_ rect: CGRect) {
    // no need to call super.draw(rect)

    let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
    let minLength = min(bounds.width, bounds.height)
    let circlePath = UIBezierPath(arcCenter: center, radius: minLength / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)

    // the following methods use current CGContext
    circleColor.set()
    circlePath.fill()
}