Calculate The Range of Visible Text in UIlabel

Calculate the range of visible text in UILabel

So I've created a method which returns the current visible string height (with the size of the UITextView / UITextField or UILabel) and it's also support iOS6+, this is what I did:

- (NSUInteger)fitString:(NSString *)string intoLabel:(UILabel *)label
{
UIFont *font = label.font;
NSLineBreakMode mode = label.lineBreakMode;

CGFloat labelWidth = label.frame.size.width;
CGFloat labelHeight = label.frame.size.height;
CGSize sizeConstraint = CGSizeMake(labelWidth, CGFLOAT_MAX);

if (SYSTEM_VERSION_GREATER_THAN(iOS_7))
{
NSDictionary *attributes = @{ NSFontAttributeName : font };
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:string attributes:attributes];
CGRect boundingRect = [attributedText boundingRectWithSize:sizeConstraint options:NSStringDrawingUsesLineFragmentOrigin context:nil];
{
if (boundingRect.size.height > labelHeight)
{
NSUInteger index = 0;
NSUInteger prev;
NSCharacterSet *characterSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];

do
{
prev = index;
if (mode == NSLineBreakByCharWrapping)
index++;
else
index = [string rangeOfCharacterFromSet:characterSet options:0 range:NSMakeRange(index + 1, [string length] - index - 1)].location;
}

while (index != NSNotFound && index < [string length] && [[string substringToIndex:index] boundingRectWithSize:sizeConstraint options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil].size.height <= labelHeight);

return prev;
}
}
}
else
{
if ([string sizeWithFont:font constrainedToSize:sizeConstraint lineBreakMode:mode].height > labelHeight)
{
NSUInteger index = 0;
NSUInteger prev;
NSCharacterSet *characterSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];

do
{
prev = index;
if (mode == NSLineBreakByCharWrapping)
index++;
else
index = [string rangeOfCharacterFromSet:characterSet options:0 range:NSMakeRange(index + 1, [string length] - index - 1)].location;
}

while (index != NSNotFound && index < [string length] && [[string substringToIndex:index] sizeWithFont:font constrainedToSize:sizeConstraint lineBreakMode:mode].height <= labelHeight);

return prev;
}
}

return [string length];
}

Of course that SYSTEM_VERSION_GREATER_THAN(iOS_7) are both macros that I defined. You also should define your own.

Best of luck!

How to calculate UILabel width based on text length?

CGSize expectedLabelSize = [yourString sizeWithFont:yourLabel.font 
constrainedToSize:maximumLabelSize
lineBreakMode:yourLabel.lineBreakMode];

What is -[NSString sizeWithFont:forWidth:lineBreakMode:] good for?

this question might have your answer, it worked for me.


For 2014, I edited in this new version, based on the ultra-handy comment by Norbert below! This does everything.

// yourLabel is your UILabel.

float widthIs =
[self.yourLabel.text
boundingRectWithSize:self.yourLabel.frame.size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{ NSFontAttributeName:self.yourLabel.font }
context:nil]
.size.width;

NSLog(@"the width of yourLabel is %f", widthIs);

finding range of visible text in UITextView

I came up with a solution on the day that I added the bounty, but did not post it so that others would have a chance to post something helpful to others. Mr. Bajorek and rintaro both posted answers that worked. My solution was a bit different; it better suits what I am trying to do in my project (since I will use multiple UITextViews). It disables scrolling in the UITextView, lets the UIScrollView handle the scrolling, and adds the UITextView with the full height needed to accommodate all of the attributed text. This allowed the calculation to work correctly from the beginning.

For the benefit of others who might read later, I post a complete working example below. I will mark this as the accepted answer since this is the route I went before the other solutions were posted.

ViewController.h

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <UIScrollViewDelegate> {
UITextView *lTextView;
UIScrollView *scrollView;
}
@end

ViewController.m

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

int statusBarHeight = 20;

scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, statusBarHeight, self.view.frame.size.width, self.view.frame.size.height)];
[self.view addSubview:scrollView];

lTextView = [[UITextView alloc] init];
lTextView.editable = NO;
lTextView.selectable = NO;
lTextView.scrollEnabled = NO;
lTextView.textContainerInset = UIEdgeInsetsZero;

lTextView.attributedText = [self loremIpsum];
float calculatedHeight = [self heightForTextViewWithAttributedText:lTextView andWidth:scrollView.frame.size.width];
lTextView.frame = CGRectMake(0, 0, scrollView.frame.size.width, calculatedHeight);
[scrollView addSubview:lTextView];

[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, calculatedHeight + 20)];
scrollView.delegate = self;

[self report];
}

- (void)report {
NSArray *visibleRange = [self visibleRangeOfTextView:lTextView];
NSNumber *start = [visibleRange objectAtIndex:0];
NSNumber *end = [visibleRange objectAtIndex:1];
int rangeLength = (end.intValue - start.intValue);

NSLog(@"%@", [lTextView.text substringWithRange:NSMakeRange(start.intValue, rangeLength)]);

}

-(NSArray *)visibleRangeOfTextView:(UITextView *)textView {
double offsetY = scrollView.contentOffset.y;
CGPoint startingPoint = scrollView.contentOffset;
startingPoint.y += 5; // to prevent including lines that can barely be seen at the top
CGPoint endingPoint = CGPointMake(320, offsetY + scrollView.frame.size.height - 20);

CGRect bounds = textView.bounds;
bounds.size.height -= 30.0; // to prevent including lines that can barely be seen at the bottom

UITextPosition *start = [textView characterRangeAtPoint:startingPoint].start;
UITextPosition *end = [textView characterRangeAtPoint:endingPoint].end;

float startOffset = [textView offsetFromPosition:textView.beginningOfDocument toPosition:start];
float endOffset = [textView offsetFromPosition:textView.beginningOfDocument toPosition:end];

return [NSArray arrayWithObjects:[NSNumber numberWithFloat:startOffset], [NSNumber numberWithFloat:endOffset], nil];
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
NSLog(@"scrollViewDidEndDecelerating");
[self report];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
NSLog(@"scrollViewDidEndDragging");
[self report];
}

- (NSAttributedString *)loremIpsum {

NSMutableAttributedString *li = [[NSMutableAttributedString alloc] init];
NSArray *strings = [NSArray arrayWithObjects:@"1 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam congue eleifend risus eget pretium. Donec sed commodo neque, id ornare dolor.", @" 2 Vivamus vestibulum non quam et euismod. Morbi et dolor luctus velit lobortis ornare vel vel tellus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aenean viverra, urna nec tempor commodo, turpis nisl rhoncus mauris, in ullamcorper justo sapien quis nulla.", @" 3 Aliquam at odio molestie, laoreet elit sed, suscipit risus. Nulla eleifend, quam eget porttitor condimentum, metus lacus lobortis ligula, accumsan tristique neque turpis non purus. Aenean malesuada tortor id elit semper, et pretium nulla viverra.", @" 4 Aliquam sollicitudin placerat massa, quis posuere est ornare vel. Nam mollis convallis risus a tincidunt. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.", @" 5 Fusce facilisis orci nisi, vel egestas metus tristique vitae. Proin nec malesuada dolor. Donec eget felis accumsan, facilisis turpis vitae, blandit lacus.", @" 6 Pellentesque auctor nisl quis turpis commodo lacinia. In sed euismod urna. Praesent sed commodo magna.", @" 7 Ut interdum dignissim urna, nec feugiat dolor. Nulla facilisi. Donec fermentum mauris at ante tincidunt, id accumsan eros lacinia.", @" 8 Suspendisse potenti. Integer ac mattis eros, sed volutpat dui. Pellentesque vehicula turpis ut metus malesuada blandit.", @" 9 Nam laoreet dui id imperdiet pulvinar. In auctor enim ac massa feugiat adipiscing. Nam convallis neque at felis tincidunt iaculis. Maecenas dictum est ac nulla suscipit, nec condimentum metus molestie.", @" 10 Vestibulum mollis velit eu nunc eleifend egestas. Ut aliquam ultrices tellus volutpat consectetur. Morbi eget sollicitudin quam, ut imperdiet leo. Morbi sed ligula iaculis, tincidunt diam nec, pharetra ligula.", @" 11 Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Proin varius facilisis placerat. Fusce suscipit risus risus, in varius diam convallis quis. In hac habitasse platea dictumst. Integer non lectus non dolor fringilla venenatis eget quis nisl. Proin in pretium metus.", @" 12 Pellentesque sed tellus iaculis, bibendum neque vel, porta ante. Phasellus eu vulputate massa. Nullam venenatis lectus non nunc aliquet porta. Nunc gravida rutrum feugiat. Fusce elit nunc, facilisis non tristique placerat, tempor id orci. Mauris et massa cursus, dapibus urna a, condimentum arcu. Pellentesque vitae sagittis sapien. Ut lacus purus, suscipit at magna non, rhoncus luctus dui. Ut ipsum augue, pharetra ac ipsum sed, facilisis convallis justo.", @" 13 Ut arcu augue, hendrerit vel tincidunt vitae, aliquet ac quam. Nulla ullamcorper, dolor eu pellentesque cursus, lectus quam interdum ante, nec congue dui augue nec dolor. Sed convallis elit in enim dictum, at posuere sem mollis. Praesent in metus aliquam, ullamcorper purus tempor, mattis ipsum. Aliquam gravida, sem vitae iaculis placerat, dui velit commodo nulla, vitae ultrices lectus dolor ut mi.", @" 14 Suspendisse quis metus varius, congue turpis vitae, viverra nunc. Duis placerat, felis et laoreet pretium, nibh lorem pulvinar turpis, eu euismod arcu libero at mauris. Sed laoreet, eros in tempor accumsan, odio augue fermentum dui, a pharetra felis libero eu ligula.", @"Suspendisse ultricies pulvinar urna. Donec placerat nulla non elit vestibulum mattis.", @"Phasellus semper sem a sem dignissim tempus. Sed scelerisque sed purus interdum rhoncus. Sed gravida eros sit amet dui fermentum rutrum. Proin a arcu scelerisque, volutpat orci quis, dignissim neque. Aliquam erat volutpat. Sed eget scelerisque neque.", nil];
for (int i = 0; i < strings.count; i++) {
NSString *string = [strings objectAtIndex:i];
if (i != 0) {
// Comment this line and it will work perfectly:
[li appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\n"]];
}
[li appendAttributedString:[[NSAttributedString alloc] initWithString:string]];
}
return li;
}

- (CGFloat)heightForTextViewWithAttributedText:(UITextView *)textView andWidth:(CGFloat)width
{
CGSize size = [textView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return size.height + 3.0;
}
@end

Get range of visible characters of NSString

The other answers have pointed out that there are possible approximations for how much text fits, there's no real way to get the actual text rendered by a UILabel because the it doesn't expose any outputs of the rendering process.

If you render your text through something like TTTAttributedLabel, you can get a look inside the rendering process and inject some code to get information about the text that was rendered.

For example, -[TTTAttributedLabel drawFramesetter:attributedString:textRange:inRect:context:] renders the actual text with CoreText by generating a CTFrameRef. CTFrameRef is tasked with laying out text and reporting what text fit and what didn't.

Ignoring truncation:

If you don't care about the fact you're losing characters to truncation, you can call CTFrameGetVisibleStringRange to get the range of characters that are displayed. The idea behind this function is that things like multi-column text layouts need to know where to begin on their next frame:

CFRange actuallyRenderedRange = CTFrameGetVisibleStringRange(frame);
NSString *actuallyRenderedText = [attributedString.string substringWithRange:NSMakeRange(actuallyRenderedRange.location, actuallyRenderedRange.length)];

Considering truncation:

Truncation is complex and a black box with regards to CoreText. For non-truncated lines, the the label can just render the line from CTFrameGetLines. For truncated lines, it must use CTLineCreateTruncatedLine, specifying the truncation line, the truncation point and the truncation token (...). Once you have this line, however, there's no direct way to get what text was chopped off since it could happen at the start, end, or middle of the line. See https://stackoverflow.com/a/5672594/860000.

If you assume end, you can force the truncation to be its own glyph run and get the end position of the second to last text run. If you compare that with the number of characters removed you can get the new string:

  1. Add a dummy attribute to the truncation string:

    NSMutableDictionary *truncationTokenStringAttributes = [self.truncationTokenStringAttributes mutableCopy];
    if (!truncationTokenStringAttributes) {
    truncationTokenStringAttributes = [[attributedString attributesAtIndex:(NSUInteger)truncationAttributePosition effectiveRange:NULL] mutableCopy];
    }
    truncationTokenStringAttributes[@"DummyAttributeToForceNewRun"] = @1;
  2. After truncatedLine is created compare the second to last run end position to the normal line length:

    CFArrayRef truncatedLineRuns = CTLineGetGlyphRuns(truncatedLine);
    CFIndex truncatedLineRunCount = CFArrayGetCount(truncatedLineRuns);
    CFIndex numberOfCharactersRemoved;
    CFIndex untruncatedLineLength = CTLineGetStringRange(line).length;
    if (truncatedLineRunCount > 1) {
    CTRunRef lastRealRun = CFArrayGetValueAtIndex(truncatedLineRuns, truncatedLineRunCount - 2);
    CFRange lastRunRange = CTRunGetStringRange(lastRealRun);
    numberOfCharactersRemoved = untruncatedLineLength - (lastRunRange.length + lastRunRange.location);
    } else {
    numberOfCharactersRemoved = untruncatedLineLength; // Everything was removed.
    }

    if (numberOfCharactersRemoved > 0) {
    actuallyRenderedText = [actuallyRenderedText substringToIndex:actuallyRenderedText.length - numberOfCharactersRemoved];
    }

    actuallyRenderedText = [actuallyRenderedText stringByAppendingString:truncationTokenString];

By the time the function completes actuallyRenderedText will contain the actual text that was rendered, including the truncation symbol. I've tried this for a variety of strings and this has come up with a consistent result.

Add ...Read More to the end of UILabel

So this is what I did to add the Read More... button to the UITextView, UITextField or UILabel:

- (void)addReadMoreStringToUILabel:(UILabel*)label
{
NSString *readMoreText = @" ...Read More";
NSInteger lengthForString = label.text.length;
if (lengthForString >= 30)
{
NSInteger lengthForVisibleString = [self fitString:label.text intoLabel:label];
NSMutableString *mutableString = [[NSMutableString alloc] initWithString:label.text];
NSString *trimmedString = [mutableString stringByReplacingCharactersInRange:NSMakeRange(lengthForVisibleString, (label.text.length - lengthForVisibleString)) withString:@""];
NSInteger readMoreLength = readMoreText.length;
NSString *trimmedForReadMore = [trimmedString stringByReplacingCharactersInRange:NSMakeRange((trimmedString.length - readMoreLength), readMoreLength) withString:@""];
NSMutableAttributedString *answerAttributed = [[NSMutableAttributedString alloc] initWithString:trimmedForReadMore attributes:@{
NSFontAttributeName : label.font
}];

NSMutableAttributedString *readMoreAttributed = [[NSMutableAttributedString alloc] initWithString:readMoreText attributes:@{
NSFontAttributeName : Font(TWRegular, 12.),
NSForegroundColorAttributeName : White
}];

[answerAttributed appendAttributedString:readMoreAttributed];
label.attributedText = answerAttributed;

UITagTapGestureRecognizer *readMoreGesture = [[UITagTapGestureRecognizer alloc] initWithTarget:self action:@selector(readMoreDidClickedGesture:)];
readMoreGesture.tag = 1;
readMoreGesture.numberOfTapsRequired = 1;
[label addGestureRecognizer:readMoreGesture];

label.userInteractionEnabled = YES;
}
else {

NSLog(@"No need for 'Read More'...");

}
}

There is a use of fitString:intoLabel method which can be found here.

As for the UITagTapGestureRecognizer is just a normal UITapGestureRecognizer subclass with a NSInteger property called tag. I did that because I want to identify which Read More... were clicked in I case I have more than one in the same UIViewController. You can use a normal UITapGestureRecognizer.

Enjoy!

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

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.



Related Topics



Leave a reply



Submit