Search code examples
swiftuikituilabelcore-graphicsnsattributedstring

How to achieve a thick stroked border around text UILabel


This is what I am trying to achieve:

enter image description here

Heres a few things I tried:

NSAttributedString

I have tried using NSAttributedString with the below attributes, but there seems to be a bug with text paths in iOS 14+

private var attributes: [NSAttributedString.Key: Any] {
    [
        .strokeColor : strokeColor,
        .strokeWidth : -8,
        .foregroundColor : foregroundColor,
        .font: UIFont.systemFont(ofSize: 17, weight: .black)
    ]
}

NSAttributedString

I'd be perfectly fine with this result from NSAttributedString if it didn't have that weird pathing issue with some of the letters.

Draw Text Override

I have also tried to override drawText, but as far as I can tell, I cant find any way of changing the stroke thickness and having it blend together with the next character:

override func drawText(in rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()
    context?.setLineJoin(.round)
    
    context?.setTextDrawingMode(.stroke)
    self.textColor = strokeColor
    super.drawText(in: rect)
    
    context?.setTextDrawingMode(.fill)
    self.textColor = foregroundColor
    super.drawText(in: rect)
    
    layer.shadowColor = UIColor.black.cgColor
    layer.shadowRadius = 2
    layer.shadowOpacity = 0.08
    layer.shadowOffset = .init(width: 0, height: 2)
}

enter image description here


Solution

  • Use this designable class to render labels with the stroke on the storyboard. Most of the fonts I tried look bad (with CGLineJoin.miter), I found the "PingFang TC" font most closely resembles the desired output. Though CGLineJoin.round lineJoin looks fine on most of the font.

    @IBDesignable
    class StrokeLabel: UILabel {
        @IBInspectable var strokeSize: CGFloat = 0
        @IBInspectable var strokeColor: UIColor = .clear
      
        override func drawText(in rect: CGRect) {
            let context = UIGraphicsGetCurrentContext()
            let textColor = self.textColor
            context?.setLineWidth(self.strokeSize)
            context?.setLineJoin(CGLineJoin.miter)
            context?.setTextDrawingMode(CGTextDrawingMode.stroke)
            self.textColor = self.strokeColor
            super.drawText(in: rect)
            context?.setTextDrawingMode(.fill)
            self.textColor = textColor
            super.drawText(in: rect)
        }
    }
    

    Output: (Check used values in the attribute inspector for reference)

    enter image description here