Search code examples
iosswiftuilabelnsattributedstringnsrange

How do you bold the first line in an NSMutableParagraphStyle?


I have a class called "rectangle" to make custom UILabels. I override "draw" in the rectangle class. When I instantiate the label, I want the FIRST line of text to show up in bolded font. I know how to solve this by manually getting the range for each string... however, I have more than 300 strings to do. The strings are currently in an array, formatted like so: "Happy \n Birthday". How can I make the word "Happy" bold?

var messageText = "Happy \n Birthday"
let rectanglePath = UIBezierPath(rect: rectangleRect)
context.saveGState()
UIColor.white.setFill()
rectanglePath.fill()
context.restoreGState()

darkPurple.setStroke()
rectanglePath.lineWidth = 0.5
rectanglePath.lineCapStyle = .square
rectanglePath.lineJoinStyle = .round
rectanglePath.stroke()

let rectangleStyle = NSMutableParagraphStyle()
rectangleStyle.alignment = .center
let rectangleFontAttributes = [
  .font: UIFont.myCustomFont(true),
  .foregroundColor: UIColor.black,
  .paragraphStyle: rectangleStyle,
  ] as [NSAttributedString.Key: Any]

let rectangleTextHeight: CGFloat = messageText.boundingRect(with: CGSize(width: rectangleRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: rectangleFontAttributes, context: nil).height
context.saveGState()
context.clip(to: rectangleRect)
messageText.draw(in: CGRect(x: rectangleRect.minX, y: rectangleRect.minY + (rectangleRect.height - rectangleTextHeight) / 2, width: rectangleRect.width, height: rectangleTextHeight), withAttributes: rectangleFontAttributes)
context.restoreGState()

Solution

  • You can find the first by separating the string by newline:

    let firstLine = "Happy \n Birthday".split(separator: "\n").first
    

    This will give you the first line of the string. (long text multi lining doesn't count) then you can find the range using this and apply the bold effect.

    How this works:

    • You need to set the label the way that accepts multiline:
    • Find the range of first line
    • Convert it to nsRange
    • Apply attributes to the range

    Here is a fully working example:

    import UIKit
    import PlaygroundSupport
    
    extension StringProtocol where Index == String.Index {
    
        func nsRange(from range: Range<Index>) -> NSRange {
            return NSRange(range, in: self)
        }
    }
    
    class MyViewController : UIViewController {
        override func loadView() {
            let view = UIView()
            view.backgroundColor = .white
    
            let label = UILabel()
            label.numberOfLines = 0
            label.text = "Happy \n Birthday"
            label.textColor = .black
    
            let text = "Happy \n Birthday"
            let attributedString = NSMutableAttributedString(string: text)
            let firstLine = text.split(separator: "\n").first!
            let range = text.range(of: firstLine)!
            attributedString.addAttributes([.font : UIFont.boldSystemFont(ofSize: 14)], range: text.nsRange(from: range))
            label.attributedText = attributedString
            label.sizeToFit()
    
            view.addSubview(label)
            self.view = view
        }
    }
    
    PlaygroundPage.current.liveView = MyViewController()