How to truncate UITextView contents to fit a reduced size? - ios

I'm having real trouble getting my head around this issue.
As the title suggests, I have several UITextViews on a view in an iPhone application. I am programmatically creating them and successfully filling that textview with text, but in some cases the text I put in the view takes up more space than the frame I allocate for it. In this case I would like the text to be truncated, but I can't figure out how to do it.
I have predefined the following constants;
#define viewOriginX 20
#define viewOriginY 180
Here is my UITextView creation code;
textViewOne = [[UITextView alloc] initWithFrame:CGRectMake(viewOriginX, viewOriginY + 65, 280, 45];
textViewOne.delegate = self;
textViewOne.scrollEnabled = NO;
textViewOne.contentInset = UIEdgeInsetsZero;
textViewOne.font = viewFont;
textViewOne.textColor = [UIColor blackColor];
textViewOne.textAlignment = UITextAlignmentLeft;
[self.view addSubview:textViewOne];
In some cases I have 15 to 20 lines of text in here and I would like to truncate it to 2 lines.
Can anyone help me in the right direction?
Thanks in advance! :D

Original Answer (iOS 6 and below)
Sadly, UILabel with numberOfLines wont do it if you need the view to be editable. Or you want UITextView's (native) vertical alignment.
Here's an NSString category that deletes words from a string, according to it's size in a rect:
#interface NSString (StringThatFits)
- (NSString *)stringByDeletingWordsFromStringToFit:(CGRect)rect
withInset:(CGFloat)inset
usingFont:(UIFont *)font
#end
#implementation NSString (StringThatFits)
- (NSString *)stringByDeletingWordsFromStringToFit:(CGRect)rect
withInset:(CGFloat)inset
usingFont:(UIFont *)font
{
NSString *result = [self copy];
CGSize maxSize = CGSizeMake(rect.size.width - (inset * 2), FLT_MAX);
CGSize size = [result sizeWithFont:font
constrainedToSize:maxSize
lineBreakMode:UILineBreakModeWordWrap];
NSRange range;
if (rect.size.height < size.height)
while (rect.size.height < size.height) {
range = [result rangeOfString:#" "
options:NSBackwardsSearch];
if (range.location != NSNotFound && range.location > 0 ) {
result = [result substringToIndex:range.location];
} else {
result = [result substringToIndex:result.length - 1];
}
size = [result sizeWithFont:font
constrainedToSize:maxSize
lineBreakMode:UILineBreakModeWordWrap];
}
return result;
}
#end
For a UITextView, use an inset of 8 to account for the way UIKit draws them:
CGRect rect = aTextView.frame;
NSString *truncatedString = [theString stringByDeletingWordsFromStringToFit:rect
withInset:8.f
usingFont:theTextView.font];
Updated Answer (iOS 7)
Now UITextView uses TextKit internally, it's much easier.
Rather than truncating the actual string, set the text (or attributedText) property to the whole string and truncate the amount of text displayed in the container (just like we do with UILabel):
self.textView.scrollEnabled = NO;
self.textView.textContainer.maximumNumberOfLines = 0;
self.textView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail;

You can do a character count. For example, if you have UITextField like this:
+--------------------+
|This is a string in |
|a text view. |
+--------------------+
you have something like 20 characters per line. If you know that number you can simply truncate your string by -substringToIndex: method.
int maxCharacters = 40; // change it to your max
if([myString length] > maxCharacters) {
myString = [myString substringToIndex:maxCharacters];
}
You can also think about UILabel. Since you need only 2 lines of text, UILabel's numberOfLines property can solve your problem.

Because sizeWithFont:constrainedToSize:lineBreakMode: is deprecated in iOS 7, I made a few changes:
- (NSString *)stringByDeletingWordsFromStringToFit:(CGRect)rect
withInset:(CGFloat)inset
usingFont:(UIFont *)font
{
NSString *result = [self copy];
CGSize maxSize = CGSizeMake(rect.size.width - (inset * 2), FLT_MAX);
if (!font) font = [UIFont systemFontOfSize:[UIFont systemFontSize]];
CGRect boundingRect = [result boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName: font, } context:nil];
CGSize size = boundingRect.size;
NSRange range;
if (rect.size.height < size.height)
while (rect.size.height < size.height) {
range = [result rangeOfString:#" "
options:NSBackwardsSearch];
if (range.location != NSNotFound && range.location > 0 ) {
result = [result substringToIndex:range.location];
} else {
result = [result substringToIndex:result.length - 1];
}
if (!font) font = [UIFont systemFontOfSize:[UIFont systemFontSize]];
CGRect boundingRect = [result boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName: font, } context:nil];
size = boundingRect.size;
}
return result;
}

Here's a peace of code i wrote to truncate a string (by words) to fit a certain width and height:
- (NSString *)stringByTruncatingString:(NSString *)string toHeight:(CGFloat)height maxWidth:(CGFloat)width withFont: (UIFont *)font {
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
NSDictionary *attrDict = [NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil];
NSMutableString *truncatedString = [string mutableCopy];
if ([string sizeWithAttributes: #{NSFontAttributeName: font}].height > height) {
// this line is optional, i wrote for better performance in case the string is too big
if([truncatedString length] > 150) truncatedString = [[truncatedString substringToIndex:150] mutableCopy];
// keep removing the last word until string is short enough
while ([truncatedString boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingTruncatesLastVisibleLine|NSStringDrawingUsesLineFragmentOrigin attributes:attrDict context:nil].size.height > height) {
NSRange wordrange= [truncatedString rangeOfString: #" " options: NSBackwardsSearch];
truncatedString = [[truncatedString substringToIndex: wordrange.location] mutableCopy];
}
// add elipsis to the end
truncatedString = [NSMutableString stringWithFormat:#"%#...",truncatedString];
}
return truncatedString;
}
Usage:
NSString *smallString = [self stringByTruncatingString:veryBigString toHeight:65 maxWidth:200 withFont:[UIFont systemFontSize:14.0f]];

Related

troubles calculating the max label height

Im having troubles calculating the max label height of a group of labels. I have the following code:
-(double)calcMaxHeight:(NSDictionary *)fields withFont:(UIFont *)font {
double maxRowHeigth = 0;
NSArray *keys = [fields allKeys];
for(NSString *key in keys) {
NSDictionary *dic = [fields objectForKey:key];
UILabel *aux = [[UILabel alloc] init];
[aux setNumberOfLines:0];
[aux setText:[dic objectForKey:#"text"]];
[aux setFont:font];
[aux setLineBreakMode:NSLineBreakByWordWrapping];
CGSize maxSize = CGSizeMake([[dic objectForKey:#"value"] doubleValue],MAXFLOAT);
CGSize auxSize = [aux sizeThatFits:maxSize];
if(auxSize.height > maxRowHeigth)
maxRowHeigth = auxSize.height;
}
return maxRowHeigth;
}
And this code "works". The problem is that i need to calculate the label size when the text has paddings because, in the drawTextInRect method, i add an UIEdgeInsetsof {3,3,3,3}.
So, how can i calculate the UILabel size counting with the padding? I tried overriding the sizeThatFits method, but im missing something yet:
-(CGSize)sizeThatFits:(CGSize)size {
NSString *text=self.text;
double width = size.width - 6;
CGSize nSize = CGSizeMake(width, size.height);
CGSize stringSize = [text boundingRectWithSize:nSize
options:NSStringDrawingTruncatesLastVisibleLine |
NSStringDrawingUsesLineFragmentOrigin
attributes:#{NSFontAttributeName : self.font}
context:nil].size;
return stringSize;
}
With this, i cant see the whole text when it occupies more than 1 row.
Thank you.
Regards.
This will give you the height of the string and therefore the label:
- (CGSize)sizeForString:(NSString *)text font:(UIFont *)font maxHeight:(CGFloat)maxHeight maxWidth:(CGFloat)maxWidth {
NSDictionary *stringAttributes = #{NSFontAttributeName : font};
CGSize stringLength = [text boundingRectWithSize:CGSizeMake(maxWidth, maxHeight
options:NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin
attributes:stringAttributes
context:nil].size;
return stringLength;
}

How do I replace (…) with (…More) when text is too long

I've create a UILabel.When the text is too long, I hope to use (.. more) to replace (...).I have already tried it,but I did not demand results.
UILabel * writtenContentLabel = [[UILabel alloc]initWithFrame:CGRectMake(10, 0, ScreenWidth - 20 , 70)];
writtenContentLabel.numberOfLines = 0;
writtenContentLabel.text = [str stringByAppendingString:#"More"];
writtenContentLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
[self addSubview:writtenContentLabel];
How do I do?
You have to use a custom label view to achieve this.
Replace your UILabels with any of the following custom labels. Both of them support custom truncationTokenString method.
MDHTMLLabel https://github.com/mattdonnelly/MDHTMLLabel (I recommend this control)
TTTAtributtedLabel https://github.com/mattt/TTTAttributedLabel
You can change the default ellipsis using,
[label setTruncationTokenString:#"...More"];
You can use the following method:
- (NSString*)stringWithElipsisMore:(NSString*)oringinalStr withLabel:(UILabel*)label
{
CGRect rect = label.frame;
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
NSDictionary * attributes = #{NSFontAttributeName:label.font, NSParagraphStyleAttributeName:paragraphStyle};
const CGSize maxSize = CGSizeMake(rect.size.width, CGFLOAT_MAX);
CGSize stringSize = [oringinalStr boundingRectWithSize:maxSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attributes
context:nil].size;
if (stringSize.height > rect.size.height) {
float proportion = rect.size.height / stringSize.height;
NSString *subStr = [oringinalStr substringToIndex:oringinalStr.length * proportion];
CGSize stringSize = [subStr boundingRectWithSize:maxSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attributes
context:nil].size;
while (stringSize.height < rect.size.height) {
subStr = [oringinalStr substringToIndex:subStr.length + 1];
stringSize = [subStr boundingRectWithSize:maxSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attributes
context:nil].size;
}
subStr = [subStr substringToIndex:subStr.length - 1];
while (stringSize.height > rect.size.height){
subStr = [subStr substringToIndex:subStr.length - 1];
stringSize = [subStr boundingRectWithSize:maxSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attributes
context:nil].size;
}
NSRange range = NSMakeRange(subStr.length - 7, 7);
NSString *subSubStr = [subStr stringByReplacingCharactersInRange:range withString:#"...more"];
return subSubStr;
} else {
return oringinalStr;
}
}
NSString *subStr = [self stringWithElipsisMore:str withLabel:writtenContentLabel];
writtenContentLabel.text = subStr;

Can I figure out the numberOfLines of NSString without creating UILabel?

I just want to know number of lines, given the font, constraints, and the text. Can I figure it out without creating a UILabel?
+ (int)numberOfLines:(NSDictionary *)data{
NSString *myString = [some string calculation];
CGSize sizeConstrain = CGSizeMake(some constrain calculation);
CGSize stringSize = [myString sizeWithFont:someFont constrainedToSize:sizeConstrain];
CGRect labelFrame = CGRectMake(0,
0,
stringSize.width,
stringSize.height + 2);
UILabel *label = [[UILabel alloc]initWithFrame:labelFrame];
label.text = myString;
return label.numberOfLines;
}
Yes
+ (int)numberOfLines:(NSDictionary *)data{
NSString *myString = [some string calculation];
CGSize sizeConstrain = CGSizeMake(some constrain calculation);
CGSize stringSize = [myString sizeWithFont:someFont constrainedToSize:sizeConstrain];
return (stringSize.height/someFont.lineHeight);
}
EDIT: I used this for UITextView and iOS7
- (CGFloat) getRowsForText:(NSString*) text{
CGFloat fixedWidth = 300;
UIFont *font = [UIFont fontWithName:#"HelveticaNeue" size:14];
NSMutableParagraphStyle *paragrapStyle = [[NSMutableParagraphStyle alloc] init];
paragrapStyle.alignment = NSTextAlignmentLeft;
textStepAttr = [NSDictionary dictionaryWithObjectsAndKeys:
font,NSFontAttributeName,
paragrapStyle, NSParagraphStyleAttributeName,
nil];
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:textStepAttr];
CGRect rect = [attributedText boundingRectWithSize:CGSizeMake(fixedWidth, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
return (rect.size.height / font.lineHeight) ;
}

In UITextView, how to get the point that next input will begin another line

I'm using UITextView in iOS, and would like to get just the moment/point that next input text will begin another new line.
How can i get this?
-----Editted-------
----After a long time research, i found the below is OK, but it seems a little complicated.
//limit text within text box
NSString *originalStr = textView.text;
textView.text = [textView.text stringByAppendingString:text];
CGFloat lineHeight = textView.font.lineHeight;
int currentLines = textView.contentSize.height / lineHeight;
int maxLines = textView.frame.size.height/lineHeight;
if (currentLines > maxLines) {
NSString * firstHalfString = [originalStr substringToIndex:range.location];
NSString * secondHalfString = [originalStr substringFromIndex: range.location];
textView.text = [NSString stringWithFormat: #"%#%#%#",
firstHalfString,
text,
secondHalfString];
int timeout = 0;
while (true) {
timeout =50;
textView.text = [textView.text substringToIndex:(textView.text.length -1)];
int newCurrentLines = textView.contentSize.height / lineHeight;
if ((newCurrentLines == maxLines) || (timeout > 50))
break;
}
textView.selectedRange = range;
return false;
} else {
textView.text = originalStr;
textView.selectedRange = range;
return true;
}
In this delegate method of the UITextView, this code will count the number of lines each time the text field is changed :
-(void)textViewDidChange:(UITextView *)textView
{
UIFont *font = [UIFont boldSystemFontOfSize:11.0];
CGSize size = [string sizeWithFont:font
constrainedToSize:myUITextView.frame.size
lineBreakMode:UILineBreakModeWordWrap]; // default mode
float numberOfLines = size.height / font.lineHeight;
}

get position of enumerated substring of UILabel

I need a help with getting position of substring. I have enumerated substrings of UILabel, one randomly is changed to "aaaaa" and color to clear. I want to put UIImage in x,y of that string. I can easy get width and height of that string but I can't get origin.
Here is my code:
if (cloudWord !=nil)
{
[label.text enumerateSubstringsInRange:NSMakeRange(0, [label.text length]) options:NSStringEnumerationByWords usingBlock:^(NSString* word, NSRange wordRange, NSRange enclosingRange, BOOL* stop)
{
if ([word isEqualToString:#"aaaaa"])
{
NSLog(#"cloud");
[labelText addAttribute:NSForegroundColorAttributeName value:[UIColor clearColor] range:wordRange];
[label setAttributedText:labelText];
UIImageView *cloudImageView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:#"playBtn.png"]];
[cloudImageView setFrame:CGRectMake(labelText.size.width, labelText.size.height, 40, 40)];
[self addSubview:cloudImageView];
}
}];
}
First find the range of your substring:
NSRange range = [myString rangeOfString:subString];
Then take the substring that precedes that range (from index 0 through the index before the first character of your substring) and find the width of that substring when rendered in the UILabel's font:
NSString *prefix = [myString substringToIndex:range.location];
CGSize size = [prefix sizeWithFont:[UIFont systemFontOfSize:fontSize]];
CGPoint p = CGPointMake(size.width, 0);
This will leave you with p as the (x,y) coordinate of the upper left corner of the substring within your UILabel. You may need to convert that to the coordinate system of your label's superview, depending on how you plan to display the image.
This assumes it's a 1-line UILabel. It gets much more complicated if your UILabel spans multiple lines...
If I am right You want to know the location of substring in your string, for that you can use this
NSRange range = [myString rangeOfString:subString options:NSBackwardsSearch range:yourRange];
NSLog(#"%u", range.loaction);
UPDATED:
You have to handle the cases for more than one line..
-(void)method
{
NSString *replaceString = #"aaaaa";
CGSize maximumLabelSize = CGSizeMake(200, FLT_MAX);
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(20, 30, 200, 20)];
NSString * string= #"1236587465987997970957986985090fhyjkhkk aaaaa 687439";
CGSize expectedLabelSize = [string sizeWithFont:label.font constrainedToSize:maximumLabelSize lineBreakMode:label.lineBreakMode];
CGRect newFrame = label.frame;
newFrame.size.height = expectedLabelSize.height;
label.frame = newFrame;
label.text = string;
label.backgroundColor = [UIColor redColor];
label.numberOfLines = 3;
[self.view addSubview:label];
NSRange range = [label.text rangeOfString:replaceString options:NSBackwardsSearch range:NSMakeRange(0, label.text.length)];
NSString *substring = [label.text substringWithRange:NSMakeRange(0, range.location)];
float stringWidth = [substring sizeWithFont:label.font].width;
float count =0;
float imgWidth = [replaceString sizeWithFont:label.font].width;
while( stringWidth> label.frame.size.width-5)
{
stringWidth = stringWidth - label.frame.size.width;
count++;
}
float imgX = stringWidth + label.frame.origin.x;
float imgHeight = 0;
float imgY = 0;
if (count >0) {
imgY = ((label.frame.size.height/(count+1))*count)+ label.frame.origin.y-10*count;
}
else
{
imgY = label.frame.origin.y;
imgHeight = [substring sizeWithFont:label.font].height;
if(stringWidth + imgWidth > label.frame.size.width)
{
imgX = label.frame.origin.x;
imgY = imgY + (label.frame.size.height/(count+1)) -10;
}
UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(imgX,imgY , imgWidth, imgHeight)];
imgView.image = [UIImage imageNamed:#"btn_Links.png"];
[self.view addSubview:imgView];
}
}

Resources