How can I get the x, y, width and height parameters of the text inside the UILabel?

109 Views Asked by At

I'm trying to use RectangleCountour library to make a rounded rectangles with rounded corners between lines behind all the text strings separately in UILabel. The parameters (x, y, width and height) are dynamic and i have to count it for every string. When I try to count it for every line and don't match with the text inside the label or set the rectangle for all the label based on the biggest width. Here is an example of the code where I just set the random parameters:

    override func draw(_ rect: CGRect) {
            if let customFont = customFont {
                font = customFont
            }

        let rects: [CGRect] = [
            CGRect(x: 10, y: 20, width: 30, height: 40),
            CGRect(x: 20, y: 30, width: 50, height: 60),
        ]
            let contour: IsoOrientedContour = rects.contour()
            let roundedPath: CGPath = contour.cgPath(cornerRadius: customCornerRadius ?? 6)
            let uiPath = UIBezierPath(cgPath: roundedPath)

            guard let context = UIGraphicsGetCurrentContext() else { return }

            context.setFillColor(customBackgroundColor?.cgColor ?? UIColor(red: 94/255, green: 17/255, blue: 82/255, alpha: 0.58).cgColor)

            context.addPath(roundedPath)
            context.closePath()
            context.fillPath()

            super.draw(rect)
        }
    }

How it looks like

And this is what I want Expectation Seems like I have to draw the rect for every string based on its parameters, but I don't know how to count them. Or maybe you know the other libraries to implement this?

1

There are 1 best solutions below

1
Sweeper On

The demo code in RectangleContour shows how to do this with a UITextView.

let range = NSRange(0 ..< layoutManager.numberOfGlyphs)
let insets = textView.textContainerInset
var usedRects: [CGRect] = []
layoutManager.enumerateLineFragments(forGlyphRange: range) { _, usedRect, _, _, _ in
    var usedRect = usedRect
    usedRect.origin.x += insets.left
    usedRect.origin.y += insets.top
    usedRects.append(usedRect)
}
send(.setRects(usedRects))

The idea is to use NSLayoutManager.enumerateLineFragments to get all the rects used by each line.

To do the same with a UILabel, you just need to create an NSTextStorage that contains the label's text, a NSTextContainer the same size as the label, and a NSLayoutManager that lays out the text. Then you can also use NSLayoutManager.enumerateLineFragments.

Example code:

// creating the label
let label = UILabel(frame: .init(x: 75, y: 75, width: 80, height: 200))
label.text = "This is what I expected to see"
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.textAlignment = .center
label.sizeToFit() // this is important
view.addSubview(label)

// creating the container, storage and layout manager
let storage = NSTextStorage(attributedString: label.attributedText!)
let textContainer = NSTextContainer(size: label.bounds.size)
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
storage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines

// as in the demo code
let range = NSRange(0 ..< layoutManager.numberOfGlyphs)
var usedRects: [CGRect] = []
layoutManager.ensureLayout(for: textContainer)
layoutManager.enumerateLineFragments(forGlyphRange: range) { _, usedRect, _, r, _ in
    // add some padding if you want
    usedRects.append(usedRect/*.insetBy(dx: -2, dy: -2)*/)
}

// add the shape as another UIView behind the label for your desired result
let path = usedRects.contour().cgPath(cornerRadius: 10)
let backgroundView = UIView(frame: label.frame)
backgroundView.backgroundColor = .clear
let shapeLayer = CAShapeLayer()
shapeLayer.path = path
shapeLayer.fillColor = UIColor.yellow.cgColor
shapeLayer.zPosition = -100
backgroundView.layer.addSublayer(shapeLayer)
view.addSubview(backgroundView)
view.sendSubviewToBack(backgroundView)

enter image description here

Note that sizeToFit is important here because UILabels centre their text vertically, but NSTextContainers don't do that. NSLayoutManager would produce rects that are aligned to the top of the label.