How to wrap text around attachments using iOS7 Text Kit? - ios

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.

Related

Xcode UILabel bug? Line spacing cropping text with a UILabel

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;

Use something else than ellipsis (...) with NSStringDrawingTruncatesLastVisibleLine

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.

iOS Swift Wrapping Text Around Image

I have a UILabel that will contain various lengths of text. I need to place an image in the upper left corner of the text and have the text wrap around it. How can I do this? All I could find was using a UITextView which I don't want to use since it's static text.
This is a perfectly reasonable use of a UITextView. Your reasons for hesitation to use it are unclear. You can make the UITextView non-editable and non-selectable; the user will not know that it is a UITextView as opposed to to a UILabel.
If you don't like that solution, then what I would do is use, instead of a UILabel, a custom view that draws the text. You can draw the text with Text Kit and thus you can take complete charge of how the text draws. In particular, you can cause it to wrap however you like, including not drawing the text in the corner (exclusion path on the text container).
You can achieve this using NSTextAttachment and attributed text.
NSMutableAttributedString *myText = [[NSMutableAttributedString alloc] initWithString:labelStr];
NSTextAttachment *attachment = [[NSTextAttachment alloc] init]
attachment.image = yourImage;
NSAttributedString *attachmentLock = [NSAttributedString attributedStringWithAttachment:attachment];
NSMutableAttributedString *lockString = [[NSMutableAttributedString alloc] initWithAttributedString:myText];
//set your image range within the text. modify it till you get it right.
NSRange range = NSMakeRange(0,[labelStr length]);
[lockString replaceCharactersInRange:NSMakeRange(range.location, 1) withAttributedString:attachmentLock];
yourLabel.attributedText = lockString;

NSAttributedString end of first line indent

I want to have the first line in an NSAttributedString for a UITextView indented from the right side on the first line.
So the firstLineHeadIndent in NSParagraphStyle will indent the first line from the left. I want to do the same thing but from the right in my UITextView.
Here's a screenshot of how I want the text to wrap.
The Setting Text Margins article from the Text System User Interface Layer Programming Guide has this figure:
As you can see, there's no built-in mechanism to have a first line tail indent.
However, NSTextContainer has a property exclusionPaths which represents parts of its rectangular area from which text should be excluded. So, you could add a path for the upper-right corner to prevent text from going there.
UIBezierPath* path = /* compute path for upper-right portion that you want to exclude */;
NSMutableArray* paths = [textView.textContainer.exclusionPaths mutableCopy];
[paths addObject:path];
textView.textContainer.exclusionPaths = paths;
I'd suggest to create 2 different NSParagraphStyle: one specific for the first line and the second one for the rest of the text.
//Creating first Line Paragraph Style
NSMutableParagraphStyle *firstLineStyle = [[NSMutableParagraphStyle alloc] init];
[firstLineStyle setFirstLineHeadIndent:10];
[firstLineStyle setTailIndent:200]; //Note that according to the doc, it's in point, and go from the origin text (left for most case) to the end, it's more a length that a "margin" (from right) that's why I put a "high value"
//Read there: https://developer.apple.com/library/ios/documentation/Cocoa/Reference/ApplicationKit/Classes/NSMutableParagraphStyle_Class/index.html#//apple_ref/occ/instp/NSMutableParagraphStyle/tailIndent
//Creating Rest of Text Paragraph Style
NSMutableParagraphStyle *restOfTextStyle = [[NSMutableParagraphStyle alloc] init];
[restOfTextStyle setAlignement:NSTextAlignmentJustified];
//Other settings if needed
//Creating the NSAttributedString
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:originalString];
[attributedString addAttribute:NSParagraphStyleAttributeName value:firstLineStyle range:rangeOfFirstLine];
[attributedString addAttribute:NSParagraphStyleAttributeName
value:restOfTextStyle
range:NSMakeRange(rangeOfFirstLine.location+rangeOfFirstLine.length,
[originalString length]-(rangeOfFirstLine.location+rangeOfFirstLine.length))];
//Setting the NSAttributedString to your UITextView
[yourTextView setAttributedText:attributedString];

How can I horizontally center a single line of text in UITextView?

I have a UITextView which displays a fair amount of text. I need to horizontally center some, but not all, of the lines of text in the text view.
I've tried setting the NSParagraphStyleAttributeName with an NSParagraphStyle instance whose alignment property is NSTextAlignmentCenter on the range of the line to be centered. I'm also setting the font so the centered line is bold. When I do this using the code below, the relevant line is bolded, but remains left aligned.
if (shouldCenterLine) {
NSMutableParagraphStyle *centerStyle = [[NSMutableParagraphStyle alloc] init];
centerStyle.alignment = NSTextAlignmentCenter;
NSDictionary *attributes = #{NSParagraphStyleAttributeName : centerStyle,
NSFontAttributeName : boldFont};
[attributedString setAttributes:attributes range:lineRange];
}
If I apply this same paragraph style attribute to the entire contents of the text view, the text ends up centered as expected.
Is it possible to convince UITextView to center specific subranges of its text? Is the only solution to move to a UIWebView rendering generated HTML? Note that this is for an app still supporting iOS 6, so unfortunately I'm not able to use Text Kit.
Try including the line returns around the the line that you'd like centered within the range that you apply the style to.

Resources