Determine last line's width

2020-08-17 05:30发布

问题:

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

回答1:

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;
}


回答2:

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)


回答3:

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)



回答4:

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


标签: ios uilabel