How to truncate an NSString based on the graphical width? - ios

In UILabel there's functionality to truncate labels using different truncation techniques (UILineBreakMode). In NSString UIKit Additions there is a similar functionality for drawing strings.
However, I found no way to access the actual truncated string. Is there any other way to get a truncated string based on the (graphical) width for a given font?
I'd like to have a category on NSString with this method:
-(NSString*)stringByTruncatingStringWithFont:(UIFont *)font forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode

- (NSString*)stringByTruncatingStringWithFont:(UIFont *)font forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode {
NSMutableString *resultString = [[self mutableCopy] autorelease];
NSRange range = {resultString.length-1, 1};
while ([resultString boundingRectWithSize:CGSizeMake(FLT_MAX, FLT_MAX) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:attributes context:nil].size.width > width) {
// delete the last character
[resultString deleteCharactersInRange:range];
range.location--;
// replace the last but one character with an ellipsis
[resultString replaceCharactersInRange:range withString:truncateReplacementString];
}
return resultString;
}
Note that since iOS 6 this method is not safe to run on background threads anymore.

One option is trying different sizes by looping until you get the right width. I.e. start with the full string, if that's wider than what you need, replace the last two characters with an ellipsis character. Loop until it's narrow enough.
If you think you'll be working with long strings, you can binary search your way towards the truncation point to make it a bit faster.

Related

Prevent line break in a NSAttributedString

I think it's a common issue when you have a set of words where you don't want a break line.
Sometimes the character between those words is a space or a hyphen, etc. In my case it's a point :)
This is my text 50.0/80.0
At the end I did it using the size label and measuring how much space I need for that string in particular:
UIFont *fontAwardNumber = [UIFont fontWithName:#"DIN-Bold" size:20];
NSStringDrawingContext *context = [[NSStringDrawingContext alloc] init];
CGSize labelSize = (CGSize){customCell.awardValueLabel.bounds.size.width, FLT_MAX};
CGRect rectNeededForAwardNumber = [awardNumber boundingRectWithSize:labelSize options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName: fontAwardNumber} context:context];
if (rectNeededForAwardNumber.size.height > customCell.awardValueLabel.bounds.size.height) {
//We need to add a breakline
NSRange range = [awardNumber rangeOfString:#"/"];
if (range.location != NSNotFound) {
awardNumber = [awardNumber stringByReplacingCharactersInRange:range withString:#"/\n"];
}
}
I found other solutions like replacing your space or hyphen for unbreakable characters:
Preventing line breaks in part of an NSAttributedString
But my question is more general, does NSAttributedString provide something to define a set of words as non breakable? Or is there any easier way to do it for a general set of words?
No, NSAttributedString doesn't have any per-character attributes that preventing line breaking within a range. You can set the NSLineBreakMode to ByClipping or another non-wrapping mode in the NSParagraphStyle, but that applies to all the text in the paragraph. (Paragraphs are separated by newlines.)
To prevent line breaking in a smaller range than a whole paragraph, you need to insert a U+2060 WORD JOINER between any two characters where an unwanted break might occur. In your example, that means on each side of the slash character.

NSAttributedString reporting incorrect sizes for UITextView sizeThatFits and boundingRectWithSize with correct options set

I have an NSAttributedString that is reporting a boundingRectWithSize (and by extension a UITextView which improperly calculates its sizeThatFits) when the font size is decreased from the font size that was used to create it.
It doesn't happen on all NSAttributedStrings for which I do similar operations, so here's the steps to reproduce.
Use a non-standard font that does not include the full unicode character set.
Make sure the string includes characters in this "unsupported" set. iOS will render them as Helvetica in the proper size.
Scale your font down on all font attributes in your NSAttributedString. My code for doing so that produced the issue looks like this.
From inside a UITextView subclass:
NSMutableAttributedString *mutableString = [self.attributedText mutableCopy];
[mutableString enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, mutableString.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
if (value) {
UIFont *oldFont = (UIFont *)value;
UIFont *newFont = [oldFont fontWithSize:oldFont.pointSize - 1];
[mutableString removeAttribute:NSFontAttributeName range:range];
[mutableString addAttribute:NSFontAttributeName value:newFont range:range];
}
}];
self.attributedText = [mutableString copy];
I noticed that while running this code in a while loop checking sizeThatFits to know when the text is small enough to fit that I would have a race to zero occur in some circumstances. The height is being calculated as 60px for any font value smaller than what I started with, which happens to be 50px.
When NSLoging the NSAttributedString I find that there are several attributes that I did not add with the key NSOriginalFont which does not appear to be in the list of supported attributes here. What's going on with NSOriginalFont? Why is my size being calculated incorrectly?
I ended up fixing this but found a lack of information on the web about it, so I decided to document my solution here.
NSOriginalFont attributes are created when the font used doesn't support one or more characters in the string. NSAttributedString adds these attributes that track what the font was "supposed" to be before a substitution to Helvetica occurred. I could make up a situation where this is useful (an all-caps font that you sometimes run uppercaseString: on?) but it wasn't useful to me.
In fact it was harmful. As I iterated through my font related attributes to decrease the size as shown above the visible size of the text was decreasing but the NSOriginalFont attribute retained a reference to the large size.
There's no built in constant for NSOriginalFont but if you call it by name it's possible to strip it from your NSMutableAttributedString. If you do you'll begin to get proper results from sizeThatFits, boundingRectWithSize, and other similar functions assuming that you're passing the correct options.
I ended up creating a simple category method on NSMutableAttributedString, included below, that works well.
NSMutableAttributedString+StripOriginalFont.h
#interface NSMutableAttributedString (StripOriginalFont)
- (void) stripOriginalFont;
#end
NSMutableAttributedString+StripOriginalFont.m
#implementation NSMutableAttributedString (StripOriginalFont)
- (void) stripOriginalFont{
[self enumerateAttribute:#"NSOriginalFont" inRange:NSMakeRange(0, self.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
if (value){
[self removeAttribute:#"NSOriginalFont" range:range];
}
}];
}
#end
Presumably you could simply modify it to keep it "in-sync" instead of removing it entirely but it wasn't useful to me for this particular project.
For stripping in swift you can use this extension:
extension NSAttributedString {
func strippedOriginalFont() -> NSAttributedString? {
let mutableCopy = self.mutableCopy() as? NSMutableAttributedString
mutableCopy?.removeAttribute(NSAttributedStringKey(rawValue: "NSOriginalFont"), range: NSMakeRange(0, self.length))
return mutableCopy?.copy() as? NSAttributedString
}
}
Don't know if this will help solve your issue, but check out my solution for auto-sizing text UITextView here: https://stackoverflow.com/a/30400391/1664123
Create NSTextStorage object and init with the attributedString.
and calculate bounds.
NSTextStorage *attributedText = [[NSTextStorage alloc] initWithAttributedString:[[NSAttributedString alloc] initWithString:text attributes:#{NSFontAttributeName:systemFont}]];
CGRect textRect = [attributedText boundingRectWithSize:CGSizeMake(textW, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin context:nil];
I had the same problem. when I call textView.setAttributedString(), it will automatically add NSOriginalFont attribute for me which lead to the wrong size. I also use sizeThatFits to calculate height.
The reason we got the wrong size is textView use the NSOriginalFont to calculate size which is not suitable for changed NSFont.
But if we use a NSTextStorage to create the attributedString and call textView.setAttributedText, then it will not add NSOriginalFont(I don't know why, but this fixes my problem) and the calculation of size will get the right answer.
Simple Code:
func getAttributedStringForTextView(content: String) -> NSAttributedString {
var attriString = NSMutableAttributedString(string: content)
// add attributes here
...
// at last, use an NSTextStorage to wrap the result
return NSTextStorage(attributedString: attriString)
}
Hope this helps.

iOS: Column formatting for NSString not working in UITextView?

I'm working on an iPad app and I need to be able to format some output on the screen in a columnar format. This was similar to my question:
How can I use the \t [tab] operator to format a NSString in columns?
and I used that solution, unfortunately I'm not seeing the same results. My code is as follows:
NSString* titleColumn = [[NSString stringWithFormat:#"%#", displayName] stringByPaddingToLength:25 withString:#" " startingAtIndex:0];
NSString* serializedValue = [[NSMutableString alloc] init];
NSString* valueAsString = [NSString stringWithFormat:#"%.03f", value];
serializedValue = [serializedValue stringByAppendingFormat:#"%#%#", titleColumn, valueAsString];
When logging the items in the console, they are properly aligned, however, when plugging them into a UITextView, they are misaligned. This is how I'm sticking them in the text view:
NSMutableString* displayString = [[NSMutableString alloc] initWithString:#""];
for (NSString* entry in textToDisplay)
{
NSLog(#"%#", entry);
[displayString appendString:entry];
[displayString appendString:#"\n"];
}
[self.fTextPanel setText:displayString];
Any ideas on what I'm doing wrong?
EDIT:
In the log, it looks like this:
Inclination 0.000
Version 0.000
Inferior/Superior 0.000
Anterior/Posterior 0.500
And the UITextView version looks like this: http://imgur.com/vrUzybP,OYxGVd8
The reason for misaligned is the different width for each character. The best way for displaying this type of information is by using UITableView, You can use title and subTitle field for displaying or you can design a custom UITableView.
the reason is because when drawing charachters (glymphs), each character will have different widths,i.e character small c and big C will take different widths for drawing , so it wont work. Even though there are two strings with equal lengths but different characters, the drawing text will have different widths.
What i would suggest you for this is to have two textViews side by side, render titles in one textVIew and values in the next TextView.
If the number of titles is large and if you enable scrolling in textVIews, use delegate Methods of UIScrollView, and when scrollHappens in either of them, set the contentOffset of the other to make it look like single TextView.
Hope this helps.

iOS: sizeWithFont for single character is different than in a string?

I am trying to determine the precise position of a character in a UILabel, say:
(UILabel *)label.text = #"Hello!";
I'd like to determine the position of the 'o'. I thought that I could just sum the widths of all the preceding characters (or the whole preceding string) using sizeWithFont. The width value I get though is bigger by about 10% than what it should be. Summing the widths of individual letters (i.e. [#"H" sizeWithFont...] + [#"e" sizeWithFont...] + l... + l...) accumulates more error than [#"Hell" sizeWithFont...].
Is there a way of accurately determining the position of a single glyph in a string?
Many thanks.
Yes, but not in a UILabel and not using sizeWithFont:.
I recently worked with Apple Developer Support, and apparently sizeWithFont: is actually an approximation. It becomes less accurate when your text (1) wraps across multiple lines and (2) contains non-latin characters (i.e. Chinese, Arabic), both of which cause line spacing changes not captured by sizeWithFont:. So, don't rely on this method if you want 100% accuracy.
Here are two things you can do:
(1) Instead of UILabel, use a non-editable UITextView. This will support the UITextInput protocol method firstRectForRange:, which you can use to get the rect of the character you need. You could use a method like this one:
- (CGRect)rectOfCharacterAtIndex:(NSUInteger)characterIndex inTextView:(UITextView *)textView
{
// set the beginning position to the index of the character
UITextPosition *beginningPosition = [textView positionFromPosition:textView.beginningOfDocument offset:characterIndex];
// set the end position to the index of the character plus 1
UITextPosition *endPosition = [textView positionFromPosition:beginningPosition offset:1];
// get the text range between these two positions
UITextRange *characterTextRange = [textView textRangeFromPosition:beginningPosition toPosition:endPosition]];
// get the rect of the character
CGRect rectOfCharacter = [textView firstRectForRange:characterTextRange];
// return the rect, converted from the text input view (unless you want it to be relative the text input view)
return [textView convertRect:rectOfCharacter fromView:textView.textInputView];
}
To use it, (assuming you have a UITextView called myTextView already on the screen), you would do this:
myTextView.text = #"Hello!";
CGRect rectOfOCharacter = [self rectOfCharacterAtIndex:4 inTextView:myTextView];
// do whatever you need with rectOfOCharacter
Only use this method for determining the rect for ONE character. The reason for this is that in the event of a line break, firstRectForRange: only returns the rect on the first line, before the break.
Also, consider adding the method above as a UITextView category if you're gong to be using it a lot. Don't forget to add error handling!
You can learn more about how firstRectForRange: works "under the hood" by reading the Text, Web, and Editing Programming Guide for iOS.
(2) Create your own UILabel by subclassing UIView and using Core Text to render the strings. Since you're doing the rendering, you'll be able to get the positions of characters. This approach is a lot of work, and only worthwhile if you really need it (I, of course, don't know the other needs of your app). If you aren't sure how this would work, I suggest using the first approach.
Well fonts are smart now a day and take in respect the position of a character to its pervious character.
Here is an example on how the starting position of the letter o:
NSRange posRange = [hello rangeOfString:#"o"];
NSString *substring = [hello substringToIndex:posRange.location];
CGSize size = [substring sizeWithFont:[UIFont systemFontOfSize:14.0f]];
No you can do the same for the string including the letter o and substract the size found in the string without the letter o.
THis should give the an nice start position of the letter and the size.
in ios6 you can do using attributed string
NSMutableAttributedString *titleText2 = [[NSMutableAttributedString alloc] initWithString:strHello];
NSRange posRange = [hello rangeOfString:#"o"];
[titleText2 addAttributes:[NSDictionary dictionaryWithObject:[UIFont systemFontOfSize:14.0f] forKey:NSFontAttributeName] range:NameRange];
and set your textView with this attributed string

Dynamic UILabel truncating the text

I'm using code provided in this answer to create a dynamic label and it works for the most part. But whenever the label text goes over 94 characters in length it gets truncated and ellipsis' are added.
There is one more odd thing about this is that if I add more characters to the string they are shown but the last 2 lines are still truncated.
Eg.
The string:
this is a very very long string
with lots of words to test the
dynamic bubble sizing one two three.
shows up like this:
this is a very very long string
with lots of words to test the
dynamic bubble sizing one tw...
But when I double the string by using the same sentence again in the label it show more of te text but still truncates it.
Eg.
The string:
this is a very very long string
with lots of words to test the
dynamic bubble sizing one two
three. this is a very very long
string with lots of words to test
the dynamic bubble sizing one
two three.
shows like this:
this is a very very long string
with lots of words to test the
dynamic bubble sizing one two
three. this is a very very long
string with lots of words to tes...
Here's the code I'm using.
NSString *temp = [NSString stringWithFormat:#"this is a very very long string with lots of words to test the dynamic bubble sizing one two three"];
captionLabel.text = temp;
//Calculate the expected size based on the font and linebreak mode of your label
CGSize maximumLabelSize = CGSizeMake(296,9999);
CGSize expectedLabelSize = [temp sizeWithFont:captionLabel.font
constrainedToSize:maximumLabelSize
lineBreakMode:captionLabel.lineBreakMode];
//adjust the label the the new height.
CGRect newFrame = captionLabel.frame;
newFrame.size.height = expectedLabelSize.height;
captionLabel.frame = newFrame;
Hope someone has an idea because this has me scratching my head.
EDIT
Using captionLabel.frame.size.width instead of hard-coded 296 fixed it, thanks to #troolee, if he/she chooses to create an answer I will mark it correct.
I was hoping #troolee would have made his comment an answer by now but since he hasn't I'm going to post the answer and mark it correct so I can close off this question.
Using captionLabel.frame.size.width instead of hard-coded 296 fixed it.
Instead of captionLabel.lineBreakMode , just write UILineBreakModeWordWrap. It should work.
Try the following UILabel Category. Thanks for the creator.
UILabel+VAlign.h
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#interface UILabel (VAlign)
- (void) setVerticalAlignmentTopConstrainedToSize:(CGSize)size;
#end
UILabel+VAlign.h
#import "UILabel+VAlign.h"
#implementation UILabel (VAlign)
- (void) setVerticalAlignmentTopConstrainedToSize:(CGSize)size
{
CGSize textSize = [self.text sizeWithFont:self.font
constrainedToSize:size
lineBreakMode:self.lineBreakMode];
CGRect textRect = CGRectMake(self.frame.origin.x,
self.frame.origin.y,
self.frame.size.width,
textSize.height);
[self setFrame:textRect];
[self setNeedsDisplay];
}
#end

Resources