How to get text / String from nth line of UILabel?
Asked Answered
S

10

20

Is there an easy way to get (or simply display) the text from a given line in a UILabel?

My UILabel is correctly displaying my text and laying it out beautifully but occasionally I need to be able to just show certain lines but obviously I need to know how UILabel has positioned everything to do this.

I know this could easily be done with a substring but I'd need to know the start and end point of the line.

Alternatively I could scroll the UILabel if there was some kind of offset to the UILabel's frame and hide the rest of the content I didn't want to see.

I've not been able to uncover anything that shows how this could be done easily. Anyone got any good ideas?

Thanks

iphaaw

Sonni answered 12/12, 2010 at 10:42 Comment(0)
B
4

I don't think there's a native way for doing this (like a "takethenline" method).
I can figure out a tricky solution but I'm not sure is the best one.
You could split your label into an array of words.
Then you could loop the array and check the text height until that word like this:

NSString *texttocheck;
float old_height = 0;
int linenumber = 0; 

for (x=0; x<[wordarray lenght]; x++) {
    texttocheck = [NSString stringWithFormat:@"%@ %@", texttocheck, [wordarray objectAtIndex:x]];

    float height = [text sizeWithFont:textLabel.font
                    constrainedToSize:CGSizeMake(textLabel.bounds.size.width,99999) 
                        lineBreakMode:UILineBreakModeWordWrap].height;

    if (old_height < height) {
        linenumber++;
    }
}

If height changes, it means there's a line break before the word.
I can't check if the syntax is written correctly now, so you have to check it yourself.

Birdman answered 12/12, 2010 at 13:27 Comment(2)
This would'n work if text contains line breaks \nFarad
Looking at my almost 9-year old code, I assume by splitting the string into words you are missing the \n special character. If I had to fix this, I would probably try to make sure the characters are included in the wordarray.Birdman
C
36

I have better way to find it.

You can get this with the help of CoreText.framework.

1.Add CoreText.framework.
2.Import #import <CoreText/CoreText.h>.
Then use below method:

- (NSArray *)getLinesArrayOfStringInLabel:(UILabel *)label {
    NSString *text = [label text];
    UIFont   *font = [label font];
    CGRect    rect = [label frame];
    
    CTFontRef myFont = CTFontCreateWithName((__bridge CFStringRef)([font fontName]), [font pointSize], NULL);
    NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:text];
    [attStr addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id)myFont range:NSMakeRange(0, attStr.length)];
    
    
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attStr);
    
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(0,0,rect.size.width,100000));
    
    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, NULL);
    
    NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
    NSMutableArray *linesArray = [[NSMutableArray alloc]init];
    
    for (id line in lines)
    {
        CTLineRef lineRef = (__bridge CTLineRef )line;
        CFRange lineRange = CTLineGetStringRange(lineRef);
        NSRange range = NSMakeRange(lineRange.location, lineRange.length);
        
        NSString *lineString = [text substringWithRange:range];
        [linesArray addObject:lineString];
    }

    return (NSArray *)linesArray;
}

Call this method :-

NSArray *linesArray = [self getLinesArrayOfStringInLabel:yourLabel];

Now you can use linesArray.

SWIFT 4 VERSION

func getLinesArrayOfString(in label: UILabel) -> [String] {
        
        /// An empty string's array
        var linesArray = [String]()
        
        guard let text = label.text, let font = label.font else {return linesArray}
        
        let rect = label.frame
        
        let myFont = CTFontCreateWithFontDescriptor(font.fontDescriptor, 0, nil)
        let attStr = NSMutableAttributedString(string: text)
        attStr.addAttribute(kCTFontAttributeName as NSAttributedString.Key, value: myFont, range: NSRange(location: 0, length: attStr.length))
        
        let frameSetter: CTFramesetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
        let path: CGMutablePath = CGMutablePath()
        path.addRect(CGRect(x: 0, y: 0, width: rect.size.width, height: 100000), transform: .identity)
        
        let frame: CTFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
        guard let lines = CTFrameGetLines(frame) as? [Any] else {return linesArray}
        
        for line in lines {
            let lineRef = line as! CTLine
            let lineRange: CFRange = CTLineGetStringRange(lineRef)
            let range = NSRange(location: lineRange.location, length: lineRange.length)
            let lineString: String = (text as NSString).substring(with: range)
            linesArray.append(lineString)
        }
        return linesArray
 }

Use:

let lines: [String] = getLinesArrayOfString(in: label)
Chillon answered 19/1, 2013 at 10:42 Comment(4)
VERSION FOR SWIFT?Hautesalpes
@SazzadHissainKhan Swift version is now available.Chillon
This function fails in some cases where the text is truncated in the label. If this happens then the lines are not correctly added to the array because the last word of the line is moved to the line before :/Lauren
@MatíasContrerasSelman Thanks for finding this issue. Actually busy days are going on, so if you found any solution contribution will be really appreciated. Also please share the "text" of label as well.Chillon
O
9

Swift 3

func getLinesArrayFromLabel(label:UILabel) -> [String] {
  let text:NSString = label.text! as NSString // TODO: Make safe?
  let font:UIFont = label.font
  let rect:CGRect = label.frame
    
  let myFont:CTFont = CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil)
  let attStr:NSMutableAttributedString = NSMutableAttributedString(string: text as String)
  attStr.addAttribute(String(kCTFontAttributeName), value:myFont, range: NSMakeRange(0, attStr.length))
  let frameSetter:CTFramesetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
  let path:CGMutablePath = CGMutablePath()
  path.addRect(CGRect(x:0, y:0, width:rect.size.width, height:100000))
    
  let frame:CTFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
  let lines = CTFrameGetLines(frame) as NSArray
  var linesArray = [String]()
    
  for line in lines {
    let lineRange = CTLineGetStringRange(line as! CTLine)
    let range:NSRange = NSMakeRange(lineRange.location, lineRange.length)
    let lineString = text.substring(with: range)
    linesArray.append(lineString as String)
  }
  return linesArray
}

Swift 2 (Xcode 7) version (tested, and re-edited from the Swift 1 answer)

func getLinesArrayOfStringInLabel(label:UILabel) -> [String] {  
  let text:NSString = label.text! // TODO: Make safe?
  let font:UIFont = label.font
  let rect:CGRect = label.frame
    
  let myFont:CTFontRef = CTFontCreateWithName(font.fontName, font.pointSize, nil)
  let attStr:NSMutableAttributedString = NSMutableAttributedString(string: text as String)
  attStr.addAttribute(String(kCTFontAttributeName), value:myFont, range: NSMakeRange(0, attStr.length))
  let frameSetter:CTFramesetterRef = CTFramesetterCreateWithAttributedString(attStr as CFAttributedStringRef)
  let path:CGMutablePathRef = CGPathCreateMutable()
  CGPathAddRect(path, nil, CGRectMake(0, 0, rect.size.width, 100000))
  let frame:CTFrameRef = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
  let lines = CTFrameGetLines(frame) as NSArray
  var linesArray = [String]()
    
  for line in lines {
    let lineRange = CTLineGetStringRange(line as! CTLine)
    let range:NSRange = NSMakeRange(lineRange.location, lineRange.length)
    let lineString = text.substringWithRange(range)
    linesArray.append(lineString as String)
  }
  return linesArray
}
Obliteration answered 30/6, 2015 at 2:4 Comment(3)
You should replace the follow code let text:NSString = label.text! // TODO: Make safe? with guard let text: NSString = self.text else { return [] }Op
At first it didn't work for me - the number of lines computed was more than actual number. but then I changed the line with CGPathAddRect to: CGPathAddRect(path, nil, CGRectMake(0, 0, rect.size.width+15, 100000)) and it worked like it supposed to.Stricker
this is unreliable. see: #46923539Leavenworth
T
5

Answer with Proper release !!!!

-(NSArray *)getLinesArrayOfStringInLabel:(UILabel *)label
{
    NSString *text = [label text];
    UIFont   *font = [label font];
    CGRect    rect = [label frame];

    CTFontRef myFont = CTFontCreateWithName(( CFStringRef)([font fontName]), [font pointSize], NULL);
    NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:text];
    [attStr addAttribute:(NSString *)kCTFontAttributeName value:( id)myFont range:NSMakeRange(0, attStr.length)];

    CFRelease(myFont);

    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString(( CFAttributedStringRef)attStr);

    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(0,0,rect.size.width,100000));

    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, NULL);

    NSArray *lines = ( NSArray *)CTFrameGetLines(frame);

    NSMutableArray *linesArray = [[NSMutableArray alloc]init];

    for (id line in lines)
    {
        CTLineRef lineRef = ( CTLineRef )line;
        CFRange lineRange = CTLineGetStringRange(lineRef);
        NSRange range = NSMakeRange(lineRange.location, lineRange.length);

        NSString *lineString = [text substringWithRange:range];

        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attStr, lineRange, kCTKernAttributeName, (CFTypeRef)([NSNumber numberWithFloat:0.0]));
        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attStr, lineRange, kCTKernAttributeName, (CFTypeRef)([NSNumber numberWithInt:0.0]));

        //NSLog(@"''''''''''''''''''%@",lineString);
        [linesArray addObject:lineString];

    }
    [attStr release];

    CGPathRelease(path);
    CFRelease( frame );
    CFRelease(frameSetter);


    return (NSArray *)linesArray;
}
Tuppence answered 19/2, 2013 at 8:28 Comment(0)
A
5

Very important change regarding iOS 11+

Starting with iOS 11, Apple intentionally changed the behaviour of their word-wrapping feature for UILabel which effects detecting the String contents of individual lines in a multiline UILabel. By design, the word-wrapping of the UILabel now avoids orphaned text (single words in a new line), as discussed here: word wrapping in iOS 11

Because of that, the way CTFrameGetLines(frame) returns the CTLine array of all lines in the label no longer works correctly if the new word-wrapping that avoids orphaned text takes effect in a particular line. To the contrary, it results in parts of the String that by the new word wrapping design would belong to the next line instead end up in the line in focus.

A tested fix for this problem can be found in my altered version of @TheTiger's answer, which makes use of calculating the actual content size of the UILabel using sizeThatFits(size:), before using that size to create the rect / path written in Swift 4:

extension UILabel {

    /// creates an array containing one entry for each line of text the label has
    var lines: [String]? {

        guard let text = text, let font = font else { return nil }

        let attStr = NSMutableAttributedString(string: text)
        attStr.addAttribute(NSAttributedString.Key.font, value: font, range: NSRange(location: 0, length: attStr.length))

        let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
        let path = CGMutablePath()

        // size needs to be adjusted, because frame might change because of intelligent word wrapping of iOS
        let size = sizeThatFits(CGSize(width: self.frame.width, height: .greatestFiniteMagnitude))
        path.addRect(CGRect(x: 0, y: 0, width: size.width, height: size.height), transform: .identity)

        let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attStr.length), path, nil)
        guard let lines = CTFrameGetLines(frame) as? [Any] else { return nil }

        var linesArray: [String] = []

        for line in lines {
            let lineRef = line as! CTLine
            let lineRange = CTLineGetStringRange(lineRef)
            let range = NSRange(location: lineRange.location, length: lineRange.length)
            let lineString = (text as NSString).substring(with: range)
            linesArray.append(lineString)
        }
        return linesArray
    }
}

This UILabel extension returns the contents of the label as a String array with one entry per line exactly as presented to the eye of the user.

Allveta answered 14/12, 2018 at 16:6 Comment(1)
Note: you need to use ceil(_:) for height in path.addRect call to avoid missing last line for some fonts.Bonina
B
4

I don't think there's a native way for doing this (like a "takethenline" method).
I can figure out a tricky solution but I'm not sure is the best one.
You could split your label into an array of words.
Then you could loop the array and check the text height until that word like this:

NSString *texttocheck;
float old_height = 0;
int linenumber = 0; 

for (x=0; x<[wordarray lenght]; x++) {
    texttocheck = [NSString stringWithFormat:@"%@ %@", texttocheck, [wordarray objectAtIndex:x]];

    float height = [text sizeWithFont:textLabel.font
                    constrainedToSize:CGSizeMake(textLabel.bounds.size.width,99999) 
                        lineBreakMode:UILineBreakModeWordWrap].height;

    if (old_height < height) {
        linenumber++;
    }
}

If height changes, it means there's a line break before the word.
I can't check if the syntax is written correctly now, so you have to check it yourself.

Birdman answered 12/12, 2010 at 13:27 Comment(2)
This would'n work if text contains line breaks \nFarad
Looking at my almost 9-year old code, I assume by splitting the string into words you are missing the \n special character. If I had to fix this, I would probably try to make sure the characters are included in the wordarray.Birdman
T
2

This is the Swift 3 version for getting all the lines in the label. (@fredpi has a similar answer but it's only for the first line)

extension UILabel {

    func getArrayOfLinesInLabel() -> [String] {

       let text = NSString(string: self.text ?? "-- -- -- --")
       let font = self.font ?? // Your default font here
       let rect = self.frame

       let myFont = CTFontCreateWithName(font.fontName as CFString?, font.pointSize, nil)
       let attStr = NSMutableAttributedString(string: text as String)
       attStr.addAttribute(String(kCTFontAttributeName), value:myFont, range: NSRange(location: 0, length: attStr.length))
       let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
       let path = CGPath(rect: CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height), transform: nil)
       let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
       guard let lines = CTFrameGetLines(frame) as? [CTLine] else {
           return []
       }

       var linesArray = [String]()

       for line in lines {
           let lineRange = CTLineGetStringRange(line)
           let range = NSRange(location: lineRange.location, length: lineRange.length)
           let lineString = text.substring(with: range)
           linesArray.append(lineString as String)
       }

       return linesArray
   }
}
Turning answered 7/4, 2017 at 18:23 Comment(0)
Z
1

Swift 3 – Xcode 8.1

I've put together code from the previous answers to create a Swift 3, Xcode 8.1-compatible extension to UILabel returning the first line of the label.

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

To use it, simple call firstLineString on your UILabel instance like this:

let firstLine = myLabel.firstLineString
Zirconium answered 6/11, 2016 at 12:22 Comment(0)
Z
1

The accepted answer is very good.

I refactored two places:

  1. changed 10000 to CGFloat.greatestFiniteMagnitude

  2. Added it to an extension of UILabel

  3. I also want to mention, if you create the label by setting the frame it works fine. If you use autolayout then dont forgot to call

    youLabel.layoutIfNeeded()

to get correct frame size.

Here is the code:

extension UILabel {
    var stringLines: [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)
        }
    }
}
Zaslow answered 14/3, 2019 at 7:22 Comment(0)
G
0

If all your characters are displayed in the same size, i.e. they're enclosed in a box of common size, you can exploit that. (This seems to be the case with Japanese characters, for example.)

Otherwise you can query the size of each character in the display font and calculate what the line would have to be.

The only worry then is that your calculation might disagree with what Apple's doing behind the scenes - in which case, I recommend you go to the trouble of overriding the text frame drawing. Look up Core Text in the documents for this.

(I may have been doing this wrong, but I didn't find Apple's method as given in the docs was very accurate, so I did something else myself.)

Ground answered 28/3, 2011 at 7:21 Comment(0)
M
0

Sorry, my reputation is too low to place a comment. This is a comment to https://mcmap.net/q/610543/-how-to-get-text-string-from-nth-line-of-uilabel from Philipp Jahoda.

Your code snippet worked flawless, until we enabled Dynamic Type on the UILabel. When we set the text size to the largest value in the iOS Settings app, it started to miss characters in the last line of the returned array. Or even missing the last line completely with a significant amount of text.

We managed to resolve this by using a different way to get frame:

let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: self.frame.width, height: .greatestFiniteMagnitude))
let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attStr.length), path.cgPath, nil)
guard let lines = CTFrameGetLines(frame) as? [Any] else { return nil }

Now it works correctly for any Dynamic Type size.

The complete function is then:

extension UILabel {

    /// creates an array containing one entry for each line of text the label has
    var lines: [String]? {

        guard let text = text, let font = font else { return nil }

        let attStr = NSMutableAttributedString(string: text)
        attStr.addAttribute(NSAttributedString.Key.font, value: font, range: NSRange(location: 0, length: attStr.length))

        let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
        let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: self.frame.width, height: .greatestFiniteMagnitude))
        let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attStr.length), path.cgPath, nil)
        guard let lines = CTFrameGetLines(frame) as? [Any] else { return nil }

        var linesArray: [String] = []

        for line in lines {
            let lineRef = line as! CTLine
            let lineRange = CTLineGetStringRange(lineRef)
            let range = NSRange(location: lineRange.location, length: lineRange.length)
            let lineString = (text as NSString).substring(with: range)
            linesArray.append(lineString)
        }
        return linesArray
    }
}
Margiemargin answered 31/5, 2019 at 7:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.