Search code examples
iosswiftuiviewuibezierpath

How do I draw stuff under the text of a UILabel?


I created a custom UILabel subclass that has a circle in the middle, and the label's text (which is a number) will be on top of the circle.

I initially thought of doing this using layer.cornerRadius, but that will not create a circle when the label's width and height are not equal.

What I mean is, for a label with width 100 and height 50, I still want a circle with radius 50 and centre at (50, 25).

Therefore, I tried to use UIBezierPath to draw the circle. This is what I have tried:

override func draw(_ rect: CGRect) {
    super.draw(rect)
    if bounds.height > bounds.width {
        let y = (bounds.height - bounds.width) / 2
        let path = UIBezierPath(ovalIn: CGRect(x: 0, y: y, width: bounds.width, height: bounds.width))
        circleColor.setFill()
        path.fill()
    } else {
        let x = (bounds.width - bounds.height) / 2
        let path = UIBezierPath(ovalIn: CGRect(x: x, y: 0, width: bounds.height, height: bounds.height))
        circleColor.setFill()
        path.fill()
    }
}

I have put super.draw(rect) because I thought that would draw the label's text, but when I run the app, I only see the circle and not my label text.

I am very confused because why hasn't super.draw(rect) drawn the label's text?


Solution

  • The text is not seen because the "z-index" of UIBezierPaths depends on the order in which they are drawn. In other words, UIBezierPaths are drawn on top of each other.

    super.draw(rect) indeed draws the text. But when you put it as the first statement, it will get drawn first, so everything you draw after that, goes on top of the text. To fix this, you should call super.draw(rect) last:

    override func draw(_ rect: CGRect) {
        if bounds.height > bounds.width {
            let y = (bounds.height - bounds.width) / 2
            let path = UIBezierPath(ovalIn: CGRect(x: 0, y: y, width: bounds.width, height: bounds.width))
            circleColor.setFill()
            path.fill()
        } else {
            let x = (bounds.width - bounds.height) / 2
            let path = UIBezierPath(ovalIn: CGRect(x: x, y: 0, width: bounds.height, height: bounds.height))
            circleColor.setFill()
            path.fill()
        }
        super.draw(rect) // <------- here!
    }
    

    Alternatively, just subclass UIView, draw the circle in draw(_:), and add a UILabel as a subview of that. The advantage if this approach is that it does not depend on the implementation of super.draw(_:), which might change in the future,