skip to Main Content

I’m trying to apply a rounded stroke effect to text using NSMutableAttributedString in Swift. I have been able to find various NSMutableAttributedString.Key options for text attributes like color, font, and underline, but I can’t seem to find any key that allows me to apply a rounded stroke to the text.

let attributes: [NSMutableAttributedString.Key: Any] = [
    .font: font,
    .strokeWidth: borderWidthToDraw,
    .strokeColor: strokeColorToDraw,
]

For better visual support, expected & curent output has been attached.

2

Answers


  1. The stroke border will be rounded if the font has rounded ends, such as SF Rounded.

    Here I displayed an NSAttributedString in a UITextView, using the SF Rounded font.

    class MyViewController: UIViewController {
        
        @IBOutlet var textView: UITextView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let attrString = NSAttributedString(string: "TIC-TAC-TOE", attributes: [
                .font:  UIFont.systemFont(ofSize: 50, weight: .bold).rounded(),
                .strokeWidth: 5,
                .strokeColor: UIColor.red,
            ])
            
            textView.attributedText = attrString
        }
    }
    
    // From https://stackoverflow.com/a/67305343/5133585
    extension UIFont {
        func rounded() -> UIFont {
            guard let descriptor = fontDescriptor.withDesign(.rounded) else {
                return self
            }
    
            return UIFont(descriptor: descriptor, size: pointSize)
        }
    }
    

    Output:

    enter image description here

    This doesn’t look exactly the same as the picture in the question. If you want a rounded stroke but the text itself should be unrounded, I doubt you can do that with NSAttributedString alone. In the worst case, you might have to use CoreText functions like CTFontCreatePathForGlyph to get the paths of the glyphs, and change lineJoin to round.

    Login or Signup to reply.
  2. I doubt you can get your desired appearance using an attributed string.

    If you’re open to another approach, here is an example using a UILabel subclass and overriding drawText(...):

    @IBDesignable
    class StrokeLabel: UILabel {
        @IBInspectable var strokeSize: CGFloat = 0.0 { didSet { setNeedsDisplay() } }
        @IBInspectable var strokeColor: UIColor = .clear { didSet { setNeedsDisplay() } }
        
        override var intrinsicContentSize: CGSize {
            // we need leading/trailing "padding" for the stroke
            var sz = super.intrinsicContentSize
            return .init(width: sz.width + strokeSize * 2.0, height: sz.height)
        }
        
        override func drawText(in rect: CGRect) {
            guard let context = UIGraphicsGetCurrentContext() else { return }
    
            // we need leading/trailing "padding" for the stroke
            let r = rect.insetBy(dx: self.strokeSize, dy: 0.0)
    
            // save textColor
            let textColor = self.textColor
    
            // set stroke properties
            context.setLineWidth(self.strokeSize)
            context.setLineJoin(.round)
    
            // stroke the text
            self.textColor = self.strokeColor
            context.setTextDrawingMode(.stroke)
            super.drawText(in: r)
    
            // "normal" draw the text
            self.textColor = textColor
            context.setTextDrawingMode(.fill)
            super.drawText(in: r)
        }
    }
    

    Example controller:

    class TestStrokeLabelVC: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = UIColor(white: 0.975, alpha: 1.0)
            
            let testLabel = StrokeLabel()
            testLabel.font = UIFont.systemFont(ofSize: 32.0, weight: .bold)
            testLabel.textColor = .black
            testLabel.text = "TIC-TAC-TOE"
            
            testLabel.strokeSize = 5.0
            testLabel.strokeColor = .red
            
            testLabel.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(testLabel)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                testLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                testLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            ])
            
        }
        
    }
    

    Output:

    enter image description here

    Note: This StrokeLabel class is based on this answer: https://stackoverflow.com/a/69368542/6257435 with a few modifications.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search