fredpi
fredpi

Reputation: 8952

Dynamically set UILabel text alignment between .left and .justified

In my app I have a UILabel with two lines preset. I can set the text alignment to either .left or .justified.

If I set it to .left, there is no layout issue if there is enough space between the last word in a line and the maximum x position of the label. Yet, when there is not so much space, so that the last word is very near the maximum x position, it looks kinda weird, because it is not exactly right-aligned (as it would be with .justified.

If I set it to .justified, it is always aligned well, yet sometimes the distance between the individual characters looks weird.

What I'm looking for is a way to dynamically adjust the text alignment depending on the distance between the last word in the first line to the maximum x position of the label. Say, if the position of the last character of the last word is smaller than 50, I want to have text alignment .left, otherwise I'd like to have .justified. Is there any way on how to accomplish this?

Upvotes: 0

Views: 354

Answers (1)

fredpi
fredpi

Reputation: 8952

I took a quite hacky approach which takes some processing power, but it seems to work.


First of all, I fetch the string in the first line of the label using this extension:

import CoreText

extension UILabel {

   /// Returns the String displayed in the first line of the UILabel or "" if text or font is missing
   var firstLineString: String {

    guard let text = self.text else { return "" }
    guard let font = self.font else { return "" }
    let rect = self.frame

    let attStr = NSMutableAttributedString(string: text)
    attStr.addAttribute(String(kCTFontAttributeName), value: CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil), range: NSMakeRange(0, attStr.length))

    let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
    let path = CGMutablePath()
    path.addRect(CGRect(x: 0, y: 0, width: rect.size.width + 7, height: 100))
    let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)

    guard let line = (CTFrameGetLines(frame) as! [CTLine]).first else { return "" }
    let lineString = text[text.startIndex...text.index(text.startIndex, offsetBy: CTLineGetStringRange(line).length-2)]

    return lineString
  }
}

After that I calculate the width, a label with line number 1 and fixed height would require for that string using this extension:

extension UILabel {

   /// Get required width for a UILabel depending on its text content and font configuration
   class func calculateWidth(text: String, height: CGFloat, font: UIFont) -> CGFloat {

       let label = UILabel(frame: CGRect(x: 0, y: 0, width: CGFloat.greatestFiniteMagnitude, height: height))
       label.numberOfLines = 1
       label.font = font
       label.text = text
       label.sizeToFit()

       return label.frame.size.width
   }
}

Based on that, I can calculate the distance to the right and decide whether to choose text alignment .left or .justified, so the main code looks like this:

// Set text
myLabel.text = someString

// Change text alignment depending on distance to right
let firstLineString = myLabel.firstLineString
let distanceToRight = myLabel.frame.size.width - UILabel.calculateWidth(text: firstLineString, height: myLabel.frame.size.height, font: myLabel.font)
myLabel.textAlignment = distanceToRight < 20 ? .justified : .left

Upvotes: 0

Related Questions