1

UILabel with background color

I have a multiline UILabel that contains an NSAttributedString, and this has a background colour applied to give the above effect.

This much is fine but I need padding within the label to give a bit of space on the left. There are other posts on SO addressing this issue, such as by subclassing UILabel to add UIEdgeInsets. However, this merely added padding to the outside of the label for me.

Any suggestions on how padding can be added to this label?

EDIT: Apologies if this has been confusing, but the end goal is something like this...

enter image description here

  • Did you try anything? – Dilan Jan 05 '21 at 16:26
  • "within the label to give a bit of space on the left" Maybe with a `NSParagraphStyle`? Or did I misunderstood the target effect? – Larme Jan 05 '21 at 16:45
  • @Larme Thanks. Yes I tried that. Using NSParagraphStyle I could adjust headIntent to create the illusion of padding on lines other that the first, but then I would also need to use firstLineHeadIndent as well to cover the first line. Annoyingly this adds margin to the first line instead of padding. –  Jan 05 '21 at 17:07

3 Answers3

1

I used one different way. First I get all lines from the UILabel and add extra blank space at the starting position of every line. To getting all line from the UILabel I just modify code from this link (https://stackoverflow.com/a/55156954/14733292)

Final extension UILabel code:

extension UILabel {
    var addWhiteSpace: String {
        guard let text = text, let font = font else { return "" }
        let ctFont = CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil)
        let attStr = NSMutableAttributedString(string: text)
        attStr.addAttribute(kCTFontAttributeName as NSAttributedString.Key, value: ctFont, range: NSRange(location: 0, length: attStr.length))
        let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
        let path = CGMutablePath()
        path.addRect(CGRect(x: 0, y: 0, width: self.frame.size.width, height: CGFloat.greatestFiniteMagnitude), transform: .identity)
        let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
        guard let lines = CTFrameGetLines(frame) as? [Any] else { return "" }
        
        return lines.map { line in
            let lineRef = line as! CTLine
            let lineRange: CFRange = CTLineGetStringRange(lineRef)
            let range = NSRange(location: lineRange.location, length: lineRange.length)
            return "  " + (text as NSString).substring(with: range)
        }.joined(separator: "\n")
    }
}

Use:

let labelText = yourLabel.addWhiteSpace
let attributedString = NSMutableAttributedString(string: labelText)
attributedString.addAttribute(NSAttributedString.Key.backgroundColor, value: UIColor.red, range: NSRange(location: 0, length: labelText.count))
yourLabel.attributedText = attributedString
yourLabel.backgroundColor = .yellow

Edited:

The above code is worked at some point but it's not sufficient. So I created one class and added padding and one shape rect layer.

class AttributedPaddingLabel: UILabel {
    
    private let leftShapeLayer = CAShapeLayer()
    
    var leftPadding: CGFloat = 5
    var attributedTextColor: UIColor = .red
    
    override func awakeFromNib() {
        super.awakeFromNib()
        self.addLeftSpaceLayer()
        self.addAttributed()
    }
    
    override func drawText(in rect: CGRect) {
        let insets = UIEdgeInsets(top: 0, left: leftPadding, bottom: 0, right: 0)
        super.drawText(in: rect.inset(by: insets))
    }
    
    override var intrinsicContentSize: CGSize {
        let size = super.intrinsicContentSize
        return CGSize(width: size.width + leftPadding, height: size.height)
    }
    
    override var bounds: CGRect {
        didSet {
            preferredMaxLayoutWidth = bounds.width - leftPadding
        }
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        leftShapeLayer.path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: leftPadding, height: rect.height)).cgPath
    }
    
    private func addLeftSpaceLayer() {
        leftShapeLayer.fillColor = attributedTextColor.cgColor
        self.layer.addSublayer(leftShapeLayer)
    }
    
    private func addAttributed() {
        let lblText = self.text ?? ""
        let attributedString = NSMutableAttributedString(string: lblText)
        attributedString.addAttribute(NSAttributedString.Key.backgroundColor, value: attributedTextColor, range: NSRange(location: 0, length: lblText.count))
        self.attributedText = attributedString
    }
}

How to use:

class ViewController: UIViewController {
    
    @IBOutlet weak var lblText: AttributedPaddingLabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        lblText.attributedTextColor = .green
        lblText.leftPadding = 10
        
    }
}
Raja Kishan
  • 16,767
  • 2
  • 26
  • 52
  • Thanks. This didn't quite work, but thanks for sharing anyway. –  Jan 05 '21 at 17:09
  • Returning a single space gave the desired result. Was there a reason why you returned two spaces at... return " " + (text as NSString).substring(with: range) –  Jan 05 '21 at 17:27
  • 1
    I tried to add space as per developer requirements. – Raja Kishan Jan 05 '21 at 17:28
  • This is a good solution, and is something I will bear in mind. The only downside is that occassionally the space does not get applied correctly. –  Jan 11 '21 at 18:53
  • @Stamp I have added one more solution please check the updated answer. – Raja Kishan Jan 12 '21 at 05:50
  • Thanks. I will take a look this evening. –  Jan 12 '21 at 09:24
1

Based on the answer provided here: https://stackoverflow.com/a/59216224/6257435

Just to demonstrate (several hard-coded values which would, ideally, be dynamic / calculated):

class ViewController: UIViewController, NSLayoutManagerDelegate {
    
    var myTextView: UITextView!
    let textStorage = MyTextStorage()
    let layoutManager = MyLayoutManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        myTextView = UITextView()
        myTextView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(myTextView)
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            myTextView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            myTextView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            myTextView.widthAnchor.constraint(equalToConstant: 248.0),
            myTextView.heightAnchor.constraint(equalToConstant: 300.0),
        ])
    
        self.layoutManager.delegate = self
        
        self.textStorage.addLayoutManager(self.layoutManager)
        self.layoutManager.addTextContainer(myTextView.textContainer)
        
        let quote = "This is just some sample text to demonstrate the word wrapping with padding at the beginning (leading) and ending (trailing) of the lines of text."
        self.textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: quote)
        
        guard let font = UIFont(name: "TimesNewRomanPSMT", size: 24.0) else {
            fatalError("Could not instantiate font!")
        }
        let attributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.font: font]
        self.textStorage.setAttributes(attributes, range: NSRange(location: 0, length: quote.count))

        myTextView.isUserInteractionEnabled = false
        
        // so we can see the frame of the textView
        myTextView.backgroundColor = .systemTeal
    }
    
    func layoutManager(_ layoutManager: NSLayoutManager,
                       lineSpacingAfterGlyphAt glyphIndex: Int,
                       withProposedLineFragmentRect rect: CGRect) -> CGFloat { return 14.0 }
    
    func layoutManager(_ layoutManager: NSLayoutManager,
                       paragraphSpacingAfterGlyphAt glyphIndex: Int,
                       withProposedLineFragmentRect rect: CGRect) -> CGFloat { return 14.0 }
}

class MyTextStorage: NSTextStorage {
    
    var backingStorage: NSMutableAttributedString
    
    override init() {
        
        backingStorage = NSMutableAttributedString()
        super.init()
    }
    
    required init?(coder: NSCoder) {
        
        backingStorage = NSMutableAttributedString()
        super.init(coder: coder)
    }
    
    //    Overriden GETTERS
    override var string: String {
        get { return self.backingStorage.string }
    }
    
    override func attributes(at location: Int,
                             effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] {
        
        return backingStorage.attributes(at: location, effectiveRange: range)
    }
    
    //    Overriden SETTERS
    override func replaceCharacters(in range: NSRange, with str: String) {
        
        backingStorage.replaceCharacters(in: range, with: str)
        self.edited(.editedCharacters,
                    range: range,
                    changeInLength: str.count - range.length)
    }
    
    override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) {
        
        backingStorage.setAttributes(attrs, range: range)
        self.edited(.editedAttributes,
                    range: range,
                    changeInLength: 0)
    }
}

import CoreGraphics //Important to draw the rectangles

class MyLayoutManager: NSLayoutManager {
    
    override init() { super.init() }
    
    required init?(coder: NSCoder) { super.init(coder: coder) }
    
    override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
        super.drawBackground(forGlyphRange: glyphsToShow, at: origin)
        
        self.enumerateLineFragments(forGlyphRange: glyphsToShow) { (rect, usedRect, textContainer, glyphRange, stop) in
            
            var lineRect = usedRect
            lineRect.size.height = 41.0
            
            let currentContext = UIGraphicsGetCurrentContext()
            currentContext?.saveGState()
            
            // outline rectangles
            //currentContext?.setStrokeColor(UIColor.red.cgColor)
            //currentContext?.setLineWidth(1.0)
            //currentContext?.stroke(lineRect)

            // filled rectangles
            currentContext?.setFillColor(UIColor.orange.cgColor)
            currentContext?.fill(lineRect)

            currentContext?.restoreGState()
        }
    }
}

Output (teal-background shows the frame):

enter image description here

DonMag
  • 69,424
  • 5
  • 50
  • 86
-3

SwiftUI

This is just a suggestion in SwiftUI. I hope it will be useful.

Step 1: Create the content
let string =
"""
Lorem ipsum
dolor sit amet,
consectetur
adipiscing elit.
"""
Step 2: Apply attributes
let attributed: NSMutableAttributedString = .init(string: string)
attributed.addAttribute(.backgroundColor, value: UIColor.orange, range: NSRange(location: 0, length: attributed.length))
attributed.addAttribute(.font, value: UIFont(name: "Times New Roman", size: 22)!, range: NSRange(location: 0, length: attributed.length))
Step 3: Create AttributedLabel in SwiftUI using a UILabel()
struct AttributedLabel: UIViewRepresentable {
    let attributedString: NSAttributedString
    
    func makeUIView(context: Context) -> UILabel {
        let label = UILabel()
        label.lineBreakMode = .byClipping
        label.numberOfLines = 0

        return label
    }
    
    func updateUIView(_ uiView: UILabel, context: Context) {
        uiView.attributedText = attributedString
    }
}
Final step: Use it and add .padding()
struct ContentView: View {
    var body: some View {
        AttributedLabel(attributedString: attributed)
            .padding()
    }
}

This is the result: