Determine last line's width
Asked Answered
T

4

12

I have a label with multiple lines, lineBreakMode is set to UILineBreakModeWordWrap. How can I determine width of last line?

Titanate answered 17/4, 2012 at 14:21 Comment(4)
I don't know of any way to do this. But why do you want this? Perhaps if we know why you want this we can come up with another way to solve your problem.Hus
I could imagine some complicated routine where you repeatedly use NSString's sizeWithFont:constrainedToSize:lineBreakMode:, adding a word at a time, figure out what word pushes you to the next line, and then repeat this process until you get to the last line, and then a final sizeWithFont:constrainedToSize:lineBreakMode: to figure out the width of that final line.Hus
I used that approach but it looks pretty complicated so I wondered if there is some nice solution for the problem. Anyway thanks!Titanate
Hey @Titanate Have you found any better solution for that ?Penicillate
P
9

Since iOS 7.0 you can do it using this function (Maybe you'll have to tweak text container a bit more for your case):

public func lastLineMaxX(message: NSAttributedString, labelWidth: CGFloat) -> CGFloat {
    // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
    let labelSize = CGSize(width: bubbleWidth, height: .infinity)
    let layoutManager = NSLayoutManager()
    let textContainer = NSTextContainer(size: labelSize)
    let textStorage = NSTextStorage(attributedString: message)

    // Configure layoutManager and textStorage
    layoutManager.addTextContainer(textContainer)
    textStorage.addLayoutManager(layoutManager)

    // Configure textContainer
    textContainer.lineFragmentPadding = 0.0
    textContainer.lineBreakMode = .byWordWrapping
    textContainer.maximumNumberOfLines = 0

    let lastGlyphIndex = layoutManager.glyphIndexForCharacter(at: message.length - 1)
    let lastLineFragmentRect = layoutManager.lineFragmentUsedRect(forGlyphAt: lastGlyphIndex,
                                                                  effectiveRange: nil)

    return lastLineFragmentRect.maxX
}

Objective-C:

- (CGFloat)lastLineMaxXWithMessage:(NSAttributedString *)message labelWidth:(CGFloat)labelWidth
{
    CGSize labelSize = CGSizeMake(labelWidth, CGFLOAT_MAX);
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:labelSize];
    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:message];
    [layoutManager addTextContainer:textContainer];
    [textStorage addLayoutManager:layoutManager];

    textContainer.lineFragmentPadding = 0;
    textContainer.lineBreakMode = NSLineBreakByWordWrapping;
    textContainer.maximumNumberOfLines = 0;
    NSUInteger lastGlyphIndex = [layoutManager glyphIndexForCharacterAtIndex:[message length] - 1];
    CGRect lastLineFragmentRect = [layoutManager lineFragmentUsedRectForGlyphAtIndex:lastGlyphIndex effectiveRange:nil];
    return CGRectGetMaxX(lastLineFragmentRect);
}

Then you can decide if there is enough place for your date label in the last line or not

usage example:

// you definitely have to set at least the font to calculate the result
// maybe for your case you will also have to set other attributes
let attributedText = NSAttributedString(string: label.text, 
                                        attributes: [.font: label.font])
let lastLineMaxX = lastLineMaxX(message: attributedText, 
                                labelWidth: label.bounds.width)
Paderewski answered 5/9, 2018 at 17:41 Comment(0)
J
5

Here's how I did it. First put your label's lines in NSArray, and then check width of last line. In viewDidLoad:

NSArray* lines = [self getSeparatedLinesFromLbl:srcLabel];
NSString *lastLine=[lines lastObject];
float lastLineWidth=[lastLine sizeWithFont:srcLabel.font constrainedToSize:boundingSize lineBreakMode:NSLineBreakByWordWrapping].width;

And getSeparatedLinesFromLbl:

-(NSArray*)getSeparatedLinesFromLbl:(UILabel*)lbl
{
if ( lbl.lineBreakMode != NSLineBreakByWordWrapping )
{
    return nil;
}

NSMutableArray* lines = [NSMutableArray arrayWithCapacity:10];

NSCharacterSet* wordSeparators = [NSCharacterSet whitespaceAndNewlineCharacterSet];

NSString* currentLine = lbl.text;
int textLength = [lbl.text length];

NSRange rCurrentLine = NSMakeRange(0, textLength);
NSRange rWhitespace = NSMakeRange(0,0);
NSRange rRemainingText = NSMakeRange(0, textLength);
BOOL done = NO;
while ( !done )
{
    // determine the next whitespace word separator position
    rWhitespace.location = rWhitespace.location + rWhitespace.length;
    rWhitespace.length = textLength - rWhitespace.location;
    rWhitespace = [lbl.text rangeOfCharacterFromSet: wordSeparators options: NSCaseInsensitiveSearch range: rWhitespace];
    if ( rWhitespace.location == NSNotFound )
    {
        rWhitespace.location = textLength;
        done = YES;
    }

    NSRange rTest = NSMakeRange(rRemainingText.location, rWhitespace.location-rRemainingText.location);

    NSString* textTest = [lbl.text substringWithRange: rTest];

    CGSize sizeTest = [textTest sizeWithFont: lbl.font forWidth: 1024.0 lineBreakMode: NSLineBreakByWordWrapping];
    if ( sizeTest.width > lbl.bounds.size.width )
    {
        [lines addObject: [currentLine stringByTrimmingCharactersInSet:wordSeparators]];
        rRemainingText.location = rCurrentLine.location + rCurrentLine.length;
        rRemainingText.length = textLength-rRemainingText.location;
        continue;
    }

    rCurrentLine = rTest;
    currentLine = textTest;
}

[lines addObject: [currentLine stringByTrimmingCharactersInSet:wordSeparators]];

return lines;
}
Joy answered 15/5, 2013 at 6:55 Comment(0)
A
2

UPDATE Swift 4.2 (IOS 12)

extension UILabel {
func getSeparatedLines() -> [Any] {
    if self.lineBreakMode != NSLineBreakMode.byWordWrapping {
        self.lineBreakMode = .byWordWrapping
    }
    var lines = [Any]() /* capacity: 10 */
    let wordSeparators = CharacterSet.whitespacesAndNewlines
    var currentLine: String? = self.text
    let textLength: Int = (self.text?.count ?? 0)
    var rCurrentLine = NSRange(location: 0, length: textLength)
    var rWhitespace = NSRange(location: 0, length: 0)
    var rRemainingText = NSRange(location: 0, length: textLength)
    var done: Bool = false
    while !done {
        // determine the next whitespace word separator position
        rWhitespace.location = rWhitespace.location + rWhitespace.length
        rWhitespace.length = textLength - rWhitespace.location
        rWhitespace = (self.text! as NSString).rangeOfCharacter(from: wordSeparators, options: .caseInsensitive, range: rWhitespace)
        if rWhitespace.location == NSNotFound {
            rWhitespace.location = textLength
            done = true
        }
        let rTest = NSRange(location: rRemainingText.location, length: rWhitespace.location - rRemainingText.location)
        let textTest: String = (self.text! as NSString).substring(with: rTest)
        let fontAttributes: [String: Any]? = [NSAttributedString.Key.font.rawValue: font]
        let maxWidth = (textTest as NSString).size(withAttributes: [NSAttributedString.Key(rawValue: NSAttributedString.Key.font.rawValue): font]).width
        if maxWidth > self.bounds.size.width {
            lines.append(currentLine?.trimmingCharacters(in: wordSeparators) ?? "")
            rRemainingText.location = rCurrentLine.location + rCurrentLine.length
            rRemainingText.length = textLength - rRemainingText.location
            continue
        }
        rCurrentLine = rTest
        currentLine = textTest
    }
    lines.append(currentLine?.trimmingCharacters(in: wordSeparators) ?? "")
    return lines
}

var lastLineWidth: CGFloat {
    let lines: [Any] = self.getSeparatedLines()
    if !lines.isEmpty {
        let lastLine: String = (lines.last as? String)!
        let fontAttributes = [NSAttributedString.Key.font.rawValue: font]
        return (lastLine as NSString).size(withAttributes: [NSAttributedString.Key(rawValue: NSAttributedString.Key.font.rawValue): font]).width
    }
    return 0
}




}

Usage

print(yourLabel.lastLineWidth)

Swift 4.2 (IOS 12)

Annelieseannelise answered 31/7, 2017 at 8:51 Comment(0)
L
0

I found that NSLayoutManager has provide API for this:

// Returns the usage rect for the line fragment in which the given glyph is laid and (optionally) by reference the whole range of glyphs that are in that fragment.  
// This will cause glyph generation and layout for the line fragment containing the specified glyph, or if non-contiguous layout is not enabled, up to and including that line fragment.  
// Line fragment used rects are always in container coordinates.
open func lineFragmentUsedRect(forGlyphAt glyphIndex: Int, effectiveRange effectiveGlyphRange: NSRangePointer?) -> CGRect

so, I think we can do it like this:

let lastGlyphIndex = self.text.count - 1
let lastLineWidth = layoutManager.lineFragmentUsedRect(forGlyphAt: lastGlyphIndex, effectiveRange: nil).size.with
Lewert answered 30/3, 2020 at 12:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.