I am trying to render some text in background using [NSAttributedString drawWithRect:options:context:] method and I'm passing (NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading| NSStringDrawingTruncatesLastVisibleLine | NSLineBreakByWordWrapping) for the options. If my string is longer than two lines (I've calculated the max height of the rectangle for that) my text is truncated with ....
It works great, however, instead of ..., I need to truncate with ...more (with "more" at the end).
All the rendering must be done on background thread so any UI component is not possible. And please don't recommend TTTAttributedLabel because I'm trying to get away from it in the first place as it resulted in terrible performance in scrolling in my app (already tried that).
How can I use a custom truncation token when drawing a string in background thread?
May not be the most efficient thing, but I've ended up like this:
Check the size of the string with desired width and no height limit (using MAXFLOAT for the bounding rect's height in draw method):
NSStringDrawingOptions drawingOptions = (NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading);
[stringToDraw boundingRectWithSize:maximumSize options:drawingOptions context:nil].size;
As I know the font size, check the height of the resulting size and check if it's taller than a predetermined height which would indicate if it's more than two lines.
If it's more than two lines, get the index of the character at the point of the rectangle where ...more should roughly start, using a modified version of the answer at https://stackoverflow.com/a/26806991/811405 (the point is somewhere near the bottom right of the original text's rectangle's second line):
//this string spans more than two lines.
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
// init text container
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size];
textContainer.lineFragmentPadding = 0;
textContainer.maximumNumberOfLines = 2;
textContainer.lineBreakMode = NSLineBreakByClipping;
[layoutManager addTextContainer:textContainer];
CGPoint moreStartLocation = CGPointMake(size.width - 60, 30); //35 magic number
NSUInteger characterIndex = [layoutManager characterIndexForPoint:moreStartLocation
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
stringToDraw = [attributedString attributedSubstringFromRange:NSMakeRange(0, characterIndex)].mutableCopy;
[stringToDraw appendAttributedString:self.truncationText];
size = CGSizeMake(size.width, 35);
Truncate the original string to character there (optional: one can also find the last whitespace (e.g. space, newline) character from the limit and get the substring from that point to avoid word clipping). Add the "...more" to the original string. (The text can be anything, with any attributes. Just make sure it will fit into the result rectangle in two lines. I've fixed it to 60px, but one can also get the size of their desired truncation string, and use its width to find the last character precisely)
Render the new string (ending with "...more") as usual:
UIGraphicsBeginImageContextWithOptions(contextSize, YES, 0);
[stringToDraw drawWithRect:rectForDrawing options:drawingOptions context:nil];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
The good thing about this method is that, since we're not touching UIKit (UIGraphics... functions and UIImage are thread-safe) we can execute the whole process in a background thread/queue. I'm using it to prerender some text content with attributes/links etc in background, that otherwise takes a frame or two in UI thread when scrolling, and it works perfectly.
Related
I have some designs I'm following for an iOS project. The font used is Avenir with relatively tight line spacing.
Some of these labels will have dynamic text, so I can't just make the label's size larger since the size should be determined by the content.
By default line spacing for a UILabel ends up pretty large.
If I adjust the Line Height Multiple or the Max Height, the text along the top ends up cropped.
It should behave like this (Affinity Designer)...
Is there a way to handle this?
Thanks for your help!
This works for me. By adding
minimumLineHeight
let string = NSMutableAttributedString(string: venue.name)
let style = NSMutableParagraphStyle()
style.lineHeightMultiple = 0.68
style.minimumLineHeight = nameLabel.font.lineHeight
string.addAttribute(NSAttributedString.Key.paragraphStyle,
value: style,
range: NSMakeRange(0, venue.name.count))
nameLabel.attributedText = string
Unfortunately the UILabel has several quirks when it comes to vertical adjustments. A somewhat hacky solution is to move the baseline of the first line down as needed. Depending on if your string ends with a newline, and the amount of tightening you do, you might need to add one or two extra newlines also, otherwise the rendering engine will clip the last line.
The code snippet assumes that self.label already has an attributed string assigned to it, and that it has line separator character 0x2028 between the lines. This is usually true when entering multi-line text in IB.
// 0x2028 is the unicode line separator character
// Use \n instead if it is what you have
// or calculate the length of the first line in some other way
NSInteger lengthOfFirstLine = [self.label.text componentsSeparatedByString:#"\u2028"][0].length;
NSMutableAttributedString *s = [[NSMutableAttributedString alloc] initWithAttributedString:self.label.attributedText];
// Add two more blank lines so that the rendering engine doesn't clip the last line
[s appendAttributedString:[[NSAttributedString alloc] initWithString:#"\n\n"]];
// Move the baseline offset for the first line down
// the other lines will adjust to this
// 50 is a value you will have to find what looks best for you
[s addAttribute:NSBaselineOffsetAttributeName value:#(-50) range:NSMakeRange(0, lengthOfFirstLine)];
self.label.attributedText = s;
I'm drawing directly into a graphics context by constructing the iOS 7 Text Kit "stack" of classes myself and asking for the glyphs to be drawn. This is my custom UIView subclass's drawRect: implementation:
NSLayoutManager* lm = [NSLayoutManager new];
NSTextStorage* ts =
[[NSTextStorage alloc] initWithAttributedString:self.attributedText];
[ts addLayoutManager:lm];
NSTextContainer* tc =
[[NSTextContainer alloc]
initWithSize:rect.size];
[lm addTextContainer:tc];
tc.lineFragmentPadding = 0;
NSRange r = NSMakeRange(0,lm.numberOfGlyphs);
[lm drawBackgroundForGlyphRange:r atPoint:CGPointMake(0,10)];
[lm drawGlyphsForGlyphRange:r atPoint:CGPointMake(0,10)];
It seems innocent enough, but an odd thing is happening: the excess text (i.e. too much text for the size of the text container) is being drawn as an extra line across the top of the graphics context:
I can skirt the issue by limiting the range of drawn glyphs to those that fit into the text container:
NSRange r = [lm glyphRangeForTextContainer:tc];
But I feel I shouldn't have to do that. And I've also seen this problem in a UITextView (where I'm not the one issuing the drawing calls), so I'm a little worried that this is just a bug in iOS 7's TextKit.
I am using the new Text Kit API to add attachments to some attributed text:
// create an attachment for each image
NSTextAttachment* ta = [NSTextAttachment new];
ta.image = [UIImage imageNamed:#"imageName"];
// add to the attributed text string
NSAttributedString* rep = [NSAttributedString attributedStringWithAttachment:ta];
[myAttributedTextString appendAttributedString:rep];
This works fine, I can see my image rendered in the output. However, I cannot find any way to specify the image alignment, or wrap text around the image.
Any ideas?
NOTE: Text attachments are different from exclusions paths - a text attachment is part of the 'model', i.e. it is part of the attributed text string that the layout manager performs text layout on. Whereas an exclusion path is part of the view.
NSTextAttachments are treated as a single character by NSAttributedString. So, in order to adjust their alignment you must do so as you would for text. It took me hours of fiddling with attachment.bounds (which I never could get to work properly) to finally figure this out. Here's an example of how to horizontally align an NSTextAttachment.
#def BETWEEN_SECTION_SPACING 10
// creates a text attachment with an image
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
attachment.image = [UIImage imageNamed:#"sample_image.jpg"];
NSMutableAttributedString *imageAttrString = [[NSAttributedString attributedStringWithAttachment:attachment] mutableCopy];
// sets the paragraph styling of the text attachment
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init] ;
[paragraphStyle setAlignment:NSTextAlignmentCenter]; // centers image horizontally
[paragraphStyle setParagraphSpacing:BETWEEN_SECTION_SPACING]; // adds some padding between the image and the following section
[imageAttrString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [imageAttrString length])];
After this, you would append imageAttrString to an existing attributed string and perhaps append another after it. One quirk is that because the attachment is a character it is not treated as its own paragraph. In order for that to be the case you will need to surround it with \n (newline characters). Just append these to both sides of the attachment's attributed string.
Hope that helps, it took me ages to figure out.
Try setting the bounds property to the image size.
Defines the layout bounds of the receiver's graphical representation in the text coordinate system.
So it should be:
ta.bounds = (CGRect) { 0, 0, ta.image.size };
ta.bounds = (CGRect) { 0, yPadding, ta.image.size };
change yPadding you need.
It can be negative when image's height is large than line height.
Please Help..im not sure were to begin
..How do i show numbers with circles around them in my UITextView
like 1 2 3 4 .............but each number inside a circle eg http://openclipart.org/people/gsagri04/GS_Numbers.svg
i was hopping to get numbers from an array and show them on screen ....but each number living inside a circle like lotto numbers
# the moment i only have a An array [1,2,3,4],...A button .....and UItextView to show the final output
xcode 4.
If I understood your need correctly, you want to display following special characters in your textview:
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳
The easiest approach is to copy+paste these characters, and replace the numeric characters in the content string you need to display in the textview. You may write a NSString category method to handle this job, in sake of code reuse.
#Chris Chen's solution is pretty nifty, as you can change the characters on the go. But if you do not want to use the characters, you may use the following code to add circles to your textView.
UITextPosition *pos = textView.endOfDocument;// textView ~ UITextView
for (int i=0;i<words*2-1;i++){// *2 since UITextGranularityWord considers a whitespace to be a word, words = number of words in the textView.
UITextPosition *pos2 = [textView.tokenizer positionFromPosition:pos toBoundary:UITextGranularityWord inDirection:UITextLayoutDirectionLeft];
UITextRange *range = [textView textRangeFromPosition:pos toPosition:pos2];
CGRect resultFrame = [textView firstRectForRange:(UITextRange *)range ];
if (check whether word at this text position is a number){
UIView* circleView = [[UIView alloc] initWithFrame:resultFrame];
circleView.backgroundColor = [UIColor clearColor];
circleView.layer.borderColor = <color of your choice, probably same as text color>;
circleView.layer.borderWidth = <the width you want to set the border thickness to>;
circleView.layer.cornerRadius = <a float value that makes the rectangle look like a circle>;
circleView .tag = 125;
[textView circleView ];
}
pos = pos2;
}
The code should be placed in the UITextView delegate method textViewDidChange. And make sure you remove the circle view before all this code, hence the tag(125).
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