Underline text in UIlabel - ios

How can I underline a text that could be multiple lines of string?
I find some people suggest UIWebView, but it is obviously too heavy a class for just text rendering.
My thoughts was to figure out the start point and length of each string in each line.
And draw a line under it accordingly.
I meet problems at how to figure out the length and start point for the string.
I tried to use -[UILabel textRectForBounds:limitedToNumberOfLines:], this should be the drawing bounding rect for the text right?
Then I have to work on the alignment?
How can I get the start point of each line when it is center-justified and right justified?

You may subclass from UILabel and override drawRect method:
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(ctx, 207.0f/255.0f, 91.0f/255.0f, 44.0f/255.0f, 1.0f); // RGBA
CGContextSetLineWidth(ctx, 1.0f);
CGContextMoveToPoint(ctx, 0, self.bounds.size.height - 1);
CGContextAddLineToPoint(ctx, self.bounds.size.width, self.bounds.size.height - 1);
CGContextStrokePath(ctx);
[super drawRect:rect];
}
UPD:
As of iOS 6 Apple added NSAttributedString support for UILabel, so now it's much easier and works for multiple lines:
NSDictionary *underlineAttribute = #{NSUnderlineStyleAttributeName: #(NSUnderlineStyleSingle)};
myLabel.attributedText = [[NSAttributedString alloc] initWithString:#"Test string"
attributes:underlineAttribute];
If you still wish to support iOS 4 and iOS 5, I'd recommend to use TTTAttributedLabel rather than underline label manually. However if you need to underline one-line UILabel and don't want to use third-party components, code above would still do the trick.

In Swift:
let underlineAttriString = NSAttributedString(string: "attriString",
attributes: [NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue])
label.attributedText = underlineAttriString

This is what i did. It works like butter.
1) Add CoreText.framework to your Frameworks.
2) import <CoreText/CoreText.h> in the class where you need underlined label.
3) Write the following code.
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:#"My Messages"];
[attString addAttribute:(NSString*)kCTUnderlineStyleAttributeName
value:[NSNumber numberWithInt:kCTUnderlineStyleSingle]
range:(NSRange){0,[attString length]}];
self.myMsgLBL.attributedText = attString;
self.myMsgLBL.textColor = [UIColor whiteColor];

Use an attribute string:
NSMutableAttributedString* attrString = [[NSMutableAttributedString alloc] initWithString:#"Your String"]
[attrString addAttribute:(NSString*)kCTUnderlineStyleAttributeName
value:[NSNumber numberWithInt:kCTUnderlineStyleSingle]
range:(NSRange){0,[attrString length]}];
And then override the label - (void)drawTextInRect:(CGRect)aRect and render the text in something like:
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
drawingRect = self.bounds;
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, drawingRect);
textFrame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0,0), path, NULL);
CGPathRelease(path);
CFRelease(framesetter);
CTFrameDraw(textFrame, ctx);
CGContextRestoreGState(ctx);
Or better yet instead of overriding just use the OHAttributedLabel created by Olivier Halligon

I've combined some of provided answers, to create better (at least for my requirements) UILabel subclass, which supports:
multiline text with various label bounds (text can be in the middle of label frame, or accurate size)
underline
strikeout
underline/strikeout line offset
text alignment
different font sizes
https://github.com/GuntisTreulands/UnderLineLabel

People, who do not want to subclass the view (UILabel/UIButton) etc...
'forgetButton' can be replace by any lable too.
-(void) drawUnderlinedLabel {
NSString *string = [forgetButton titleForState:UIControlStateNormal];
CGSize stringSize = [string sizeWithFont:forgetButton.titleLabel.font];
CGRect buttonFrame = forgetButton.frame;
CGRect labelFrame = CGRectMake(buttonFrame.origin.x + buttonFrame.size.width - stringSize.width,
buttonFrame.origin.y + stringSize.height + 1 ,
stringSize.width, 2);
UILabel *lineLabel = [[UILabel alloc] initWithFrame:labelFrame];
lineLabel.backgroundColor = [UIColor blackColor];
//[forgetButton addSubview:lineLabel];
[self.view addSubview:lineLabel];
}

NSString *tem =self.detailCustomerCRMCaseLabel.text;
if (tem != nil && ![tem isEqualToString:#""]) {
NSMutableAttributedString *temString=[[NSMutableAttributedString alloc]initWithString:tem];
[temString addAttribute:NSUnderlineStyleAttributeName
value:[NSNumber numberWithInt:1]
range:(NSRange){0,[temString length]}];
self.detailCustomerCRMCaseLabel.attributedText = temString;
}

Another solution could be (since iOS 7) given a negative value to NSBaselineOffsetAttributeName, for example your NSAttributedString could be:
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:#"my text goes here'
attributes:#{NSFontAttributeName: [UIFont fontWithName:#"Helvetica-Regular" size:12],
NSForegroundColorAttributeName: [UIColor blackColor],
NSUnderlineStyleAttributeName: #(NSUnderlineStyleSingle), NSBaselineOffsetAttributeName: #(-3)}];
Hope this will help ;-)

NSMutableAttributedString *text = [self.myUILabel.attributedText mutableCopy];
[text addAttribute:NSUnderlineStyleAttributeName value:#(NSUnderlineStyleSingle) range:NSMakeRange(0, text.length)];
self.myUILabel.attributedText = text;

You can create a custom label with name UnderlinedLabel and edit drawRect function.
#import "UnderlinedLabel.h"
#implementation UnderlinedLabel
- (void)drawRect:(CGRect)rect
{
NSString *normalTex = self.text;
NSDictionary *underlineAttribute = #{NSUnderlineStyleAttributeName: #(NSUnderlineStyleSingle)};
self.attributedText = [[NSAttributedString alloc] initWithString:normalTex
attributes:underlineAttribute];
[super drawRect:rect];
}

Here is the easiest solution which works for me without writing additional codes.
// To underline text in UILable
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:#"Type your text here"];
[text addAttribute:NSUnderlineStyleAttributeName value:#(NSUnderlineStyleSingle) range:NSMakeRange(0, text.length)];
lblText.attributedText = text;

Sometimes we developer stuck in small designing part of any UI screen. One of the most irritating requirement is under line text. Don’t worry here is the solution.
Underlining a text in a UILabel using Objective C
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(0, 0, 320, 480)];
label.backgroundColor=[UIColor lightGrayColor];
NSMutableAttributedString *attributedString;
attributedString = [[NSMutableAttributedString alloc] initWithString:#"Apply Underlining"];
[attributedString addAttribute:NSUnderlineStyleAttributeName value:#1 range:NSMakeRange(0,
[attributedString length])];
[label setAttributedText:attributedString];
Underlining a text in UILabel using Swift
label.backgroundColor = .lightGray
let attributedString = NSMutableAttributedString.init(string: "Apply UnderLining")
attributedString.addAttribute(NSUnderlineStyleAttributeName, value: 1, range:
NSRange.init(location: 0, length: attributedString.length))
label.attributedText = attributedString

An enhanced version of the code of Kovpas (color and line size)
#implementation UILabelUnderlined
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
const CGFloat* colors = CGColorGetComponents(self.textColor.CGColor);
CGContextSetRGBStrokeColor(ctx, colors[0], colors[1], colors[2], 1.0); // RGBA
CGContextSetLineWidth(ctx, 1.0f);
CGSize tmpSize = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(200, 9999)];
CGContextMoveToPoint(ctx, 0, self.bounds.size.height - 1);
CGContextAddLineToPoint(ctx, tmpSize.width, self.bounds.size.height - 1);
CGContextStrokePath(ctx);
[super drawRect:rect];
}
#end

I have Created for multiline uilabel with underline :
For Font size 8 to 13 set int lineHeight = self.font.pointSize+3;
For font size 14 to 20 set int lineHeight = self.font.pointSize+4;
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
const CGFloat* colors = CGColorGetComponents(self.textColor.CGColor);
CGContextSetRGBStrokeColor(ctx, colors[0], colors[1], colors[2], 1.0); // RGBA
CGContextSetLineWidth(ctx, 1.0f);
CGSize tmpSize = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(self.frame.size.width, 9999)];
int height = tmpSize.height;
int lineHeight = self.font.pointSize+4;
int maxCount = height/lineHeight;
float totalWidth = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(1000, 9999)].width;
for(int i=1;i<=maxCount;i++)
{
float width=0.0;
if((i*self.frame.size.width-totalWidth)<=0)
width = self.frame.size.width;
else
width = self.frame.size.width - (i* self.frame.size.width - totalWidth);
CGContextMoveToPoint(ctx, 0, lineHeight*i-1);
CGContextAddLineToPoint(ctx, width, lineHeight*i-1);
}
CGContextStrokePath(ctx);
[super drawRect:rect];
}

Swift 4.1 ver:
let underlineAttriString = NSAttributedString(string:"attriString", attributes:
[NSAttributedStringKey.underlineStyle: NSUnderlineStyle.styleSingle.rawValue])
label.attributedText = underlineAttriString

As kovpas has shown you can use the bounding box in most cases, although it is not always guaranteed that the bounding box will fit neatly around the text. A box with a height of 50 and font size of 12 may not give the results you want depending on the UILabel configuration.
Query the UIString within the UILabel to determine its exact metrics and use these to better place your underline regardless of the enclosing bounding box or frame using the drawing code already provided by kovpas.
You should also look at UIFont's "leading" property that gives the distance between baselines based on a particular font. The baseline is where you would want your underline to be drawn.
Look up the UIKit additions to NSString:
(CGSize)sizeWithFont:(UIFont *)font
//Returns the size of the string if it were to be rendered with the specified font on a single line.
(CGSize)sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size
// Returns the size of the string if it were rendered and constrained to the specified size.
(CGSize)sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode
//Returns the size of the string if it were rendered with the specified constraints.

I use an open source line view and just added it to the button subviews:
UILabel *label = termsButton.titleLabel;
CGRect frame = label.frame;
frame.origin.y += frame.size.height - 1;
frame.size.height = 1;
SSLineView *line = [[SSLineView alloc] initWithFrame:frame];
line.lineColor = [UIColor lightGrayColor];
[termsButton addSubview:line];
This was inspired by Karim above.

Based on Kovpas & Damien Praca's Answers, here is an implementation of UILabelUnderligned which also support textAlignemnt.
#import <UIKit/UIKit.h>
#interface UILabelUnderlined : UILabel
#end
and the implementation:
#import "UILabelUnderlined.h"
#implementation DKUILabel
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
const CGFloat* colors = CGColorGetComponents(self.textColor.CGColor);
CGContextSetRGBStrokeColor(ctx, colors[0], colors[1], colors[2], 1.0); // RGBA
CGContextSetLineWidth(ctx, 1.0f);
CGSize textSize = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(200, 9999)];
// handle textAlignement
int alignementXOffset = 0;
switch (self.textAlignment) {
case UITextAlignmentLeft:
break;
case UITextAlignmentCenter:
alignementXOffset = (self.frame.size.width - textSize.width)/2;
break;
case UITextAlignmentRight:
alignementXOffset = self.frame.size.width - textSize.width;
break;
}
CGContextMoveToPoint(ctx, alignementXOffset, self.bounds.size.height - 1);
CGContextAddLineToPoint(ctx, alignementXOffset+textSize.width, self.bounds.size.height - 1);
CGContextStrokePath(ctx);
[super drawRect:rect];
}
#end

Here's another, simpler solution (underline's width is not most accurate but it was good enough for me)
I have a UIView (_view_underline) that has White background, height of 1 pixel and I update its width everytime I update the text
// It's a shame you have to do custom stuff to underline text
- (void) underline {
float width = [[_txt_title text] length] * 10.0f;
CGRect prev_frame = [_view_underline frame];
prev_frame.size.width = width;
[_view_underline setFrame:prev_frame];
}

NSUnderlineStyleAttributeName which takes an NSNumber (where 0 is no underline) can be added to an attribute dictionary.
I don't know if this is any easier. But, it was easier for my purposes.
NSDictionary *attributes;
attributes = #{NSFontAttributeName:font, NSParagraphStyleAttributeName: style, NSUnderlineStyleAttributeName:[NSNumber numberWithInteger:1]};
[text drawInRect:CGRectMake(self.contentRect.origin.x, currentY, maximumSize.width, textRect.size.height) withAttributes:attributes];

You can use this my custom label!
You can also use interface builder to set
import UIKit
class YHYAttributedLabel : UILabel{
#IBInspectable
var underlineText : String = ""{
didSet{
self.attributedText = NSAttributedString(string: underlineText,
attributes: [NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue])
}
}
}

Related

UITextView, NSAttributedString and custom attributes

I have searched a lot on Stack Overflow but I couldn't find a solution. Perhaps I just misinterpreted some answers.
I have created a UITextView and I am using NSAttributedStrings to work with the UITextView which is just fine.
Now, after adding a custom attribute, I am stuck.
Where can I hook in to render my custom attribute within the UITextView? Is there a delegate method, or will I have to create my own UITextView and overwrite a method?
You can custom NSLayoutManager, and implement it's -drawGlyphsForGlyphRange:atPoint: method.
For example, you want a custom background with a corner radius
textView init:
NSTextStorage *textStorage = [NSTextStorage new];
CustomLayoutManager *layoutManager = [[CustomLayoutManager alloc] init];
CGSize containerSize = CGSizeMake(self.view.bounds.size.width, CGFLOAT_MAX);
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:containerSize];
textContainer.widthTracksTextView = YES;
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
self.textView = [[UITextView alloc] initWithFrame:yourFrame textContainer:textContainer];
And apply your custom attribute:
NSMutableAttributedString *mAttrStr = [[NSMutableAttributedString alloc] initWithString:#"SampleText"];
[mAttrStr addAttribute:YourCustomAttributeName value:[UIColor redColor] range:NSMakeRange(0, mAttrStr.length)]; //for example, you want a custom background with a corner radius
[self.textView.textStorage appendAttributedString:mAttrStr];
In CustomLayoutManager.m
-(void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {
NSRange range = [self characterRangeForGlyphRange:glyphsToShow
actualGlyphRange:NULL];
//enumerate custom attribute in the range
[self.textStorage enumerateAttribute:YourCustomAttributeName inRange:range options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
if (value) {
UIColor *color = value; //the color set above
NSRange glyphRange = [self glyphRangeForCharacterRange:range
actualCharacterRange:NULL];
NSTextContainer *container = [self textContainerForGlyphAtIndex:glyphRange.location
effectiveRange:NULL];
//draw background
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextTranslateCTM(context, origin.x, origin.y);
[color setFill];
CGRect rect = [self boundingRectForGlyphRange:glyphRange inTextContainer:container];
//UIBezierPath with rounded
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:100];
[path fill];
CGContextRestoreGState(context);
//end draw
[super drawGlyphsForGlyphRange:range atPoint:origin];
}
else {
[super drawGlyphsForGlyphRange:range atPoint:origin];
}
}];
}
Now the 'SampleText' has a red rounded background.
Please refer this simple code snippet to set attributed string to a textview
let attributedString = NSMutableAttributedString(string:"Test string to add attributes")
attributedString.addAttributes([NSForegroundColorAttributeName:UIColor.greenColor()], range: NSMakeRange(0, attributedString.string.characters.count))
textView.attributedText = attributedString
For Objective-C
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]initWithString:#"Test string to add attributes"];
[attributedString addAttributes:#{NSForegroundColorAttributeName:[UIColor greenColor]} range:NSMakeRange(0, attributedString.string.length)];
textView.attributedText = attributedString;
Hope this helps.
If you want apply particular attributes for particular textView rather then string then you should subclass UITextView and make custom initmethod or some method that return UITextView object with specified attribute!! You can pass custom attributes as parameter in method also if attributes are change i mean not fix. and if attribute will remain same implicitly then set attributes in that class by default.

Underline UIlabel so the underlining always reaches the bounds

What I want to achieve is to have UILabel underlined but in a specific way.
I know how to make UILabel underlined, but since this is going to be a dynamic text, I don't know how long it will be.
Anytime the label enters a new line, I'd like to make the underlining align with the one above regardless of the text length.
I sketched it up to give you a better notion of what I actually try to achieve:
What is your opinion, how to approach such problem?
Should I add the white line as UIView anytime text skips to another line?
Or maybe add some whitespace in code when the text lengths is shorter than bounds of current line?
first you need to set text for you label then call this method :
- (void)underlineLabel:(UILabel*)lbl {
if (![lbl respondsToSelector:#selector(setAttributedText:)]) {
return;
}
NSMutableAttributedString *attributedText;
if (!lbl.attributedText) {
attributedText = [[NSMutableAttributedString alloc] initWithString:lbl.text];
} else {
attributedText = [[NSMutableAttributedString alloc] initWithAttributedString:lbl.attributedText];
}
long len = [lbl.text length];
[attributedText addAttribute:NSUnderlineColorAttributeName value:[UIColor grayColor] range:NSMakeRange(0,len)];
[attributedText addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:1] range:NSMakeRange(0, len)];//Underline color
lbl.attributedText = attributedText;
}
func underlineLabel(label: UILabel) {
if !lbl.respondsToSelector("setAttributedText:") {
return
}
var attributedText = NSMutableAttributedString()
if !(lbl.attributedText != nil) {
attributedText = NSMutableAttributedString(string:label.text!)
}
else {
attributedText = NSMutableAttributedString(attributedString: label.attributedText!)
}
let str = label.text;
let len = str?.characters.count;
attributedText.addAttribute(NSUnderlineColorAttributeName, value: UIColor.redColor(), range: NSMakeRange(0, len!))
attributedText.addAttribute(NSUnderlineStyleAttributeName , value:1, range: NSMakeRange(0, len!))
//Underline color
lbl.attributedText = attributedText
}
I have came up with solution with custom label class and override drawRect Method in that custom class of UIlabel.
CustomLabel.h
#import <UIKit/UIKit.h>
#interface CustomLabel : UILabel
#end
CustomLabel.m
#import "CustomLabel.h"
#implementation CustomLabel
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(ctx, 1.0f);
float Left = self.center.x - self.frame.size.width/2.0;
float Right = self.center.x + self.frame.size.width/2.0;
CGContextMoveToPoint(ctx, Left, self.bounds.size.height - 1);
CGContextAddLineToPoint(ctx, Right, self.bounds.size.height - 1);
CGContextStrokePath(ctx);
[super drawRect:rect];
}
#end
In Your Class Just import this custom Class.
#import "CustomLabel.h"
////// you can create labels now which are having underline to bounds.
-(void)CreateCustomLabel
{
CustomLabel *custom = [[CustomLabel alloc] initWithFrame:CGRectMake(0, 150, SCREEN_WIDTH-40, 50)];
custom.text = #"Your Text Here";
custom.backgroundColor = [UIColor redColor];
[self.view addSubview:custom];
}

How to know if the word within UILabel was cut

I try to develop autoshrink functionality. I have attributed text, set into a fix-size UILabel. After that I lower the font size and check if the text fits to given container size.
Problem is that UILabel ignores NSLineBreakByWordWrapping if a word is longer than container width. Resulting in I have cut tail word.
Here is the code:
- (void) setCardText:(NSString *)txt {
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:txt];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping];
[paragraphStyle setAlignment:NSTextAlignmentCenter];
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [txt length])];
self.cardLabel.attributedText = attributedString;
for (CGFloat fontSize = 40; fontSize >=5; fontSize--) {
[self.cardLabel setFont:[UIFont fontWithName:#"GothamPro-Light" size:fontSize]];
[paragraphStyle setLineSpacing:fontSize*0.3f];
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [txt length])];
self.cardLabel.lineBreakMode = NSLineBreakByWordWrapping;
[self.cardLabel sizeToFit];
if (self.cardLabel.frame.size.width <= 220) {
[self.cardLabel setFrame:CGRectMake(40, 40, 220, self.cardLabel.frame.size.height)];
}
if (self.cardLabel.frame.size.height <= 210) {
[self.cardLabel setFrame:CGRectMake(40, 40, self.cardLabel.frame.size.width, 210)];
}
if (self.cardLabel.frame.size.width <= 220 && self.cardLabel.frame.size.height <= 210) {
[self.cardLabel setFrame:CGRectMake(40, 40, 220, 210)];
break;
}
};
And here is the result (I'm sorry for screenshot in Russian): http://take.ms/kg2mG
In the third line the word is cut and its ending is moved to the next line.
I guess it happens because initially this word doesn't fit to container width and was forcibly broken in half. I suppose I need sort of cut-word-detector, which tells me to keep lowering font size. Or another guess is to oblige the UILabel to be expanded by “unfit word”. But I can't find anything that does this job.
Also I can explode given string into words and check if every single of them fits to container width. But I think this method is a wheel inventing. Is there something I missed that can easily solve my issue?
The method sizeToFit calls sizeThatFits: which returns the ‘best’ size to fit the current bounds and then resize label. So at first you constrain the label and it has to fit the given width. You can see description of NSLineBreakByWordWrapping - Wrapping occurs at word boundaries, unless the word itself doesn’t fit on a single line.
For your purposes you should allow label to fit the knowingly more wider width than it requires. But it is difficult because the task is to find the best font size and we cannot predict the width. And the best way is to find font size based on the longest word in the text.
So the algorithm:
Detect the longest word, by separating by spaces.
Iteratively, decrease the font size and calculate the size of the longest word while the word is bigger than required width.
Set calculated font to full text and call sizeThatFits.
Please, find the sample code below ("Verdana" font was used for testing)
- (void) setText {
NSString * text = #"Incidental, indirect, secondary, side rival - Побочный, косвенный, второстепенный, боковой соперник";
CGFloat maxWidth = 300.;
[self setText:text toLabel:self.label maxWidth:maxWidth];
}
- (void) setText:(NSString *)text
toLabel:(UILabel*)label
maxWidth:(CGFloat)maxWidth
{
CGFloat fontSize = [self fontSizeOfWord:[self theLongestWord:text]
initialFontSize:40.
constrainedByWidth:maxWidth];
NSMutableAttributedString * attributedString = [self attributedStringForText:text];
[self setupAttributedStirng:attributedString withFontWithSize:fontSize];
label.attributedText = attributedString;
CGRect labelFrame = label.frame;
labelFrame.size = [label sizeThatFits:[attributedString sizeAdaptedForWidth:maxWidth]];
label.frame = labelFrame;
}
- (NSString*) theLongestWord:(NSString*)text {
NSArray * words = [text componentsSeparatedByString:#" "];
NSUInteger longestLength = 0;
NSUInteger index = NSNotFound;
for(int i = 0; i < words.count; i++) {
NSString * word = words[i];
CGFloat length = word.length;
if(length > longestLength) {
longestLength = length;
index = i;
}
}
return (index != NSNotFound ? words[index] : nil);
}
- (CGFloat)fontSizeOfWord:(NSString *)word
initialFontSize:(CGFloat)initialFontSize
constrainedByWidth:(CGFloat)maxWidth
{
NSMutableAttributedString * wordString = [self attributedStringForText:word];
CGFloat fontSize = initialFontSize;
for (; fontSize >= 5.; --fontSize) {
[self setupAttributedStirng:wordString
withFontWithSize:fontSize];
CGSize wordSize = [wordString sizeAdaptedForWidth:CGFLOAT_MAX];
if(wordSize.width <= maxWidth){
break;
}
}
return fontSize;
}
- (NSMutableAttributedString*) attributedStringForText:(NSString*)text {
return (text&&text.length ? [[NSMutableAttributedString alloc] initWithString:text]:nil);
}
- (void)setupAttributedStirng:(NSMutableAttributedString *)attributedString
withFontWithSize:(CGFloat)fontSize
{
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
[paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping];
[paragraphStyle setAlignment:NSTextAlignmentCenter];
UIFont * font = [UIFont fontWithName:#"Verdana" size:fontSize];
[paragraphStyle setLineSpacing:fontSize*0.3f];
NSDictionary * attributes = #{NSParagraphStyleAttributeName: paragraphStyle,
NSFontAttributeName: font};
[attributedString addAttributes:attributes
range:NSMakeRange(0, [attributedString length])];
}
Category for NSAttributedString:
#implementation NSAttributedString (AdaptedSize)
- (CGSize) sizeAdaptedForWidth:(CGFloat)width
{
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self);
CGSize targetSize = CGSizeMake(width, CGFLOAT_MAX);
CGSize fitSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter,
CFRangeMake(0, [self length]),
NULL, targetSize, NULL);
CFRelease(framesetter);
return fitSize;
}
#end
Have you tried the UILabel.adjustsFontSizeToWidth property?

Problems with placing NSAttributedString in a PDF

I'm building a PDF document dynamically in an iOS app. I am trying to convert an HTML formatted string to an NSAttributedString, then position it on my PDF document page.
If I treat the string as a plain NSString I can set it's frame and position it anywhere on the page (html tags included in the string).
If I create an NSAttributedString using initWithString I can also place the resulting attributed string anywhere on the page.
If I convert the string to an NSAttributedString using
NSAttributedString initWithData:options:documentAttributes:error:
the conversion work perfectly, and the tags are converted to attributes. But when I try to position the string's frame on the page the text is clipped as I move the frame down and right from 0,0. I can move the frame down 20 points before the text is clipped off the top, line by line. I can move the frame right until the origin is larger than the width, when all the text disappears from the page.
Code:
#pragma mark - === PDF BUILD Methods === -
- (void)buildPDF {
self.pdfPageSize = CGSizeMake(850.0, 1100.0);
UIFont *titleFont = [UIFont fontWithName:#"Dispatch-Bold" size:24.0];
UIFont *bodyFont = [UIFont systemFontOfSize:18.0];
// Open a new PDF dcument
[self setupPDFDocumentNamed:#"testPDF" Width:self.pdfPageSize.width Height:self.pdfPageSize.height];
//cover page
[self beginPDFPage];
NSString *title = [NSString stringWithFormat:#"How to Tie a %#", self.knot.knotName];
[self addText:title withFrame:CGRectMake(0, 10, 850, 30) font:titleFont alignment:NSTextAlignmentCenter];
[self addLineWithFrame:CGRectMake(10, 40, 830, 2) withColor:[CSCColors favoritesColor]];
NSString *imageName = [NSString stringWithFormat:#"%#_0", self.knot.knotID];
[self addImage:[UIImage imageNamed:imageName] atPoint:CGPointMake(50, 50)];
// addText method works, but addHTML method does not
//[self addText:self.knot.knotDescription withFrame:CGRectMake(400, 50, 300, 500) font:bodyFont alignment:NSTextAlignmentLeft];
[self addHTMLText:self.knot.knotDescription withFrame:CGRectMake(400, 50, 300, 500) font:bodyFont alignment:NSTextAlignmentLeft];
//close out the PDF
[self finishPDF];
}
-(void)setupPDFDocumentNamed:(NSString*)name Width:(float)width Height:(float)height {
self.pdfPageSize = CGSizeMake(width, height);
NSString *newPDFName = [NSString stringWithFormat:#"%#.pdf", name];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *pdfPath = [documentsDirectory stringByAppendingPathComponent:newPDFName];
UIGraphicsBeginPDFContextToFile(pdfPath, CGRectZero, nil);
}
- (void)beginPDFPage {
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, self.pdfPageSize.width, self.pdfPageSize.height), nil);
}
- (void)finishPDF {
UIGraphicsEndPDFContext();
}
- (void)addText:(NSString*)text withFrame:(CGRect)frame font:(UIFont*)font alignment:(NSTextAlignment)alignment{
// Set up character and paragraph attributes
UIColor * cscBlueColor = [UIColor colorWithRed:0.0 green:0.53 blue:0.8 alpha:1.0];
NSMutableParagraphStyle *paraStyle = [[NSMutableParagraphStyle alloc] init];
paraStyle.lineBreakMode = NSLineBreakByWordWrapping;
paraStyle.alignment = alignment;
//Calculate frame
NSDictionary *attr = #{NSFontAttributeName:font};
CGRect textRect = [text boundingRectWithSize:frame.size options:NSStringDrawingUsesLineFragmentOrigin attributes:attr context:nil];
CGSize stringSize = textRect.size;
float textWidth = frame.size.width;
if (textWidth < stringSize.width)
textWidth = stringSize.width;
if (textWidth > _pdfPageSize.width)
textWidth = _pdfPageSize.width - frame.origin.x;
CGRect renderingRect = CGRectMake(frame.origin.x, frame.origin.y, textWidth, stringSize.height);
NSDictionary *renderingAttr = #{NSParagraphStyleAttributeName:paraStyle, NSFontAttributeName:font, NSForegroundColorAttributeName:cscBlueColor};
[text drawInRect:renderingRect withAttributes:renderingAttr];
}
- (void)addHTMLText:(NSString*)text withFrame:(CGRect)frame font:(UIFont *)font alignment:(NSTextAlignment)alignment{
NSMutableAttributedString *myString = [[NSMutableAttributedString alloc] initWithData:[text dataUsingEncoding:NSUTF8StringEncoding]
options:#{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
NSCharacterEncodingDocumentAttribute: [NSNumber numberWithInt:NSUTF8StringEncoding]}
documentAttributes:nil
error:nil];
NSRange myRange;
myRange.location = 0;
myRange.length = myString.length;
// Set up character and paragraph attributes
UIColor * cscBlueColor = [UIColor colorWithRed:0.0 green:0.53 blue:0.8 alpha:1.0];
NSMutableParagraphStyle *paraStyle = [[NSMutableParagraphStyle alloc] init];
paraStyle.lineBreakMode = NSLineBreakByWordWrapping;
paraStyle.paragraphSpacing = 10.0f;
paraStyle.alignment = alignment;
NSDictionary *renderingAttr = #{NSParagraphStyleAttributeName:paraStyle, NSFontAttributeName:font, NSForegroundColorAttributeName:cscBlueColor};
[myString addAttributes:renderingAttr range:myRange];
//Calculate frame
NSStringDrawingContext *sdctx = [[NSStringDrawingContext alloc] init];
CGRect textRect = [myString boundingRectWithSize:CGSizeMake(frame.size.width, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin context:sdctx];
CGSize stringSize = textRect.size;
float textWidth = frame.size.width;
if (textWidth < stringSize.width)
textWidth = stringSize.width;
if (textWidth > _pdfPageSize.width)
textWidth = _pdfPageSize.width - frame.origin.x;
CGRect renderingRect = CGRectMake(frame.origin.x, frame.origin.y, textWidth, stringSize.height);
// fill rect so we can visualize the frame
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [UIColor lightGrayColor].CGColor);
CGContextFillRect(context, renderingRect);
//[myString drawInRect:renderingRect];
[myString drawWithRect:renderingRect options:NSStringDrawingUsesLineFragmentOrigin context:sdctx];
}
- (void)addImage:(UIImage*)image atPoint:(CGPoint)point {
CGRect imageFrame = CGRectMake(point.x, point.y, image.size.width, image.size.height);
[image drawInRect:imageFrame];
}
- (void)addLineWithFrame:(CGRect)frame withColor:(UIColor*)color {
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(currentContext, color.CGColor);
// this is the thickness of the line
CGContextSetLineWidth(currentContext, 2);
CGPoint startPoint = frame.origin;
CGPoint endPoint = CGPointMake(frame.origin.x + frame.size.width, frame.origin.y);
CGContextBeginPath(currentContext);
CGContextMoveToPoint(currentContext, startPoint.x, startPoint.y);
CGContextAddLineToPoint(currentContext, endPoint.x, endPoint.y);
CGContextClosePath(currentContext);
CGContextDrawPath(currentContext, kCGPathFillStroke);
}
Any ideas?
Screen caps:
For the 1st two screen caps, I used
NSAttributedString initWithData:options:documentAttributes:error:
to generate the attributed string
In the first, frame is set to {400, 50, 300, 500} and text is clipped completely.
In the second the frame is {400, 50, 401, 500}, the text appears, but the 1st two lines of text is clipped at the top. If I move the frame up to 20, all lines are visible.
In the final screen shot I just used initWithString to get the attributed string, and everything works as it should, except that the HTML is not converted to attributes.
I've also cleaned up the HTML string to make sure tht the tags and class calls were not introducing some wierd attributes. The cleaned up string has the same results:
<html><body><p>Landlubbers need not apply. The Sheet Bend is an essential knot to know and one of the first knots taught to new sailors. It is very fast to tie and is useful when joining two ropes of different diameters.</p><p>Also known as Becket bend (when made fast to an eye instead of a loop), the Sheet Bend is often considered one of the most essential knots and is related in structure to the bowline. If the two free ends are not on the same side of the knot, the result is a left-handed sheet bend of significantly reduced strength.</p></body></html>
Thanks

Unwanted Vertical Padding from iOS 6 on CATextLayer

Background: I started my project in iOS 5 and built out a beautiful button with layer. I added a textLayer onto the button and center it using the following code:
float textLayerVerticlePadding = ((self.bounds.size.height - fontSize) /2);
textLayer = [[CATextLayer alloc]init];
[textLayer setFrame:CGRectOffset(self.bounds, 0, textLayerVerticlePadding)];
It works great and looks dead center until iOS 6.
Problem: iOS 6 added a space (padding) between the topmost bound and the text in textLayer. This upsets the calculation above. Is there a way to make sure that iOS 6 does not? because I would like to support both iOS 5 and 6 (for those who prefers Google Map).
Pictures:
This one is iOS 5 and the red color is the background of the textLayer (to make it more apparent)
And this one is iOS 6
Update: While im sure all the answers below are correct in their own ways, I found the post by t0rst simplest way to execute this. HelveticaNeue leaves a little space for both iOS5 and iOS6, unlike Helvetica which leaves no space on the top in iOS5 and little space in iOS6.
Update 2: Played around with it a little more, and found out the size of the little space. Without going into detail, the space is 1/6 of your font size. So to compensate for it I wrote
float textLayerVerticlePadding = ((self.bounds.size.height - fontSize) /2) - (fontSize/6);
[textLayer setFrame:CGRectOffset(self.bounds, 0, textLayerVerticlePadding)];
With that code, I get a dead center every time. Note that this is only tested with HelveticaNeue-Bold on iOS5 and iOS6. I cannot say for anything else.
In iOS 5 and before, the first baseline in a CATextLayer is always positioned down from the top of the bounds by the ascent obtained from CTLineGetTypographicBounds when passed a CTLine made with the string for the first line.
In iOS 6, this doesn't hold true for all fonts anymore. Hence, when you are positioning a CATextLayer you can no longer reliably decide where to put it to get the right visual alignment. Or can you? ...
First, an aside: when trying to work out CATextLayer's positioning behaviour a while ago in iOS 5, I tried using all combinations of cap height, ascender from UIFont, etc. before finally discovering that ascent from CTLineGetTypographicBounds was the one I needed. In the process, I discovered that a) the ascent from UIFont ascender, CTFontGetAscent and CTLineGetTypographicBounds are inconsistent for certain typefaces, and b) the ascent is frequently strange - either cropping the accents or leaving way to much space above. The solution to a) is to know which value to use. There isn't really a solution to b) other than to leave plenty of room above by offsetting CATextLayer bounds if it likely you will have accents that get clipped.
Back to iOS 6. If you avoid the worst offending typefaces (as of 6.0, and probably subject to change), you can still do programatic positioning of CATextLayer with the rest of the typefaces. The offenders are: AcademyEngravedLetPlain, Courier, HoeflerText and Palatino - visually, these families position correctly (i.e. without clipping) in CATextLayer, but none of the three ascent sources gives you a usable indication of where the baseline is placed. Helvetica and .HelveticaNeueUI (aka system font) families position correctly with baseline at the ascent given by UIFont ascender, but the other ascent sources are not of use.
Some examples from tests I did. The sample text is drawn three times in different colours. The coordinate origin is top left of grey box. Black text is drawn by CTLineDraw offset downwards by the ascent from CTLineGetTypographicBounds; transparent red is drawn by CATextLayer with bounds equal to the grey box; transparent blue is drawn with the UIKit NSString addition drawAtPoint:withFont: locating at the origin of the grey box and with the UIFont.
1) A well behaved font, Copperplate-Light. The three samples are coincident, giving maroon, and meaning that the ascents are near enough the same from all sources. Same for iOS 5 and 6.
2) Courier under iOS 5. CATextLayer positions text too high (red), but CTLineDraw with ascent from CTLineGetTypographicBounds (black) matches CATextLayer positioning - so we can place and correct from there. NSString drawAtPoint:withFont: (blue) places the text without clipping. (Helvetica and .HelveticaNeueUI behave like this in iOS 6)
3) Courier under iOS 6. CATextLayer (red) now places the text so that it is not clipped, but the positioning no longer matches the ascent from CTLineGetTypographicBounds (black) or from UIFont ascender used in NSString drawAtPoint:withFont: (blue). This is unusable for programatic positioning. (AcademyEngravedLetPlain, HoeflerText and Palatino also behave like this in iOS 6)
Hope this helps avoid some of the hours of wasted time I went through, and if you want to dip in a bit deeper, have a play with this:
- (NSString*)reportInconsistentFontAscents
{
NSMutableString* results;
NSMutableArray* fontNameArray;
CGFloat fontSize = 28;
NSString* fn;
NSString* sample = #"Éa3Çy";
CFRange range;
NSMutableAttributedString* mas;
UIFont* uifont;
CTFontRef ctfont;
CTLineRef ctline;
CGFloat uif_ascent;
CGFloat ctfont_ascent;
CGFloat ctline_ascent;
results = [NSMutableString stringWithCapacity: 10000];
mas = [[NSMutableAttributedString alloc] initWithString: sample];
range.location = 0, range.length = [sample length];
fontNameArray = [NSMutableArray arrayWithCapacity: 250];
for (fn in [UIFont familyNames])
[fontNameArray addObjectsFromArray: [UIFont fontNamesForFamilyName: fn]];
[fontNameArray sortUsingSelector: #selector(localizedCaseInsensitiveCompare:)];
[fontNameArray addObject: [UIFont systemFontOfSize: fontSize].fontName];
[fontNameArray addObject: [UIFont italicSystemFontOfSize: fontSize].fontName];
[fontNameArray addObject: [UIFont boldSystemFontOfSize: fontSize].fontName];
[results appendString: #"Font name\tUIFA\tCTFA\tCTLA"];
for (fn in fontNameArray)
{
uifont = [UIFont fontWithName: fn size: fontSize];
uif_ascent = uifont.ascender;
ctfont = CTFontCreateWithName((CFStringRef)fn, fontSize, NULL);
ctfont_ascent = CTFontGetAscent(ctfont);
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)mas, range, kCTFontAttributeName, ctfont);
ctline = CTLineCreateWithAttributedString((CFAttributedStringRef)mas);
ctline_ascent = 0;
CTLineGetTypographicBounds(ctline, &ctline_ascent, 0, 0);
[results appendFormat: #"\n%#\t%.3f\t%.3f\t%.3f", fn, uif_ascent, ctfont_ascent, ctline_ascent];
if (fabsf(uif_ascent - ctfont_ascent) >= .5f // >.5 can round to pixel diffs in display
|| fabsf(uif_ascent - ctline_ascent) >= .5f)
[results appendString: #"\t*****"];
CFRelease(ctline);
CFRelease(ctfont);
}
[mas release];
return results;
}
t0rst's answer helps me.
I think capHeight and xHeight are key.
CATextLayer *mytextLayer = [CATextLayer layer];
CGFloat fontSize = 30;
UIFont *boldFont = [UIFont boldSystemFontOfSize:fontSize];
mytextLayer.font = (__bridge CFTypeRef)(boldFont.fontName);
mytextLayer.fontSize = fontSize;
CGFloat offsetY = 0;
//if system version is grater than 6
if(([[[UIDevice currentDevice] systemVersion] compare:#"6" options:NSNumericSearch] == NSOrderedDescending)){
offsetY = -(boldFont.capHeight - boldFont.xHeight);
}
//you have to set textX, textY, textWidth
mytextLayer.frame = CGRectMake(textX, textY + offsetY, textWidth, fontSize);
Wile I am waiting for an ultimate solution, I studied about RTLabel and TTTAttributedLabel, and made a simple class to draw text on a CALayer as Steve suggested. Hope it helps, and please don't hesitant to point out any mistake I have made.
CustomTextLayer.h
#import <QuartzCore/QuartzCore.h>
#interface CustomTextLayer : CALayer {
NSString *_text;
UIColor *_textColor;
NSString *_font;
float _fontSize;
UIColor *_strokeColor;
float _strokeWidth;
CTTextAlignment _textAlignment;
int _lineBreakMode;
float _suggestHeight;
}
-(float) suggestedHeightForWidth:(float) width;
#property (nonatomic, retain) NSString *text;
#property (nonatomic, retain) UIColor *textColor;
#property (nonatomic, retain) NSString *font;
#property (nonatomic, assign) float fontSize;
#property (nonatomic, retain) UIColor *strokeColor;
#property (nonatomic, assign) float strokeWidth;
#property (nonatomic, assign) CTTextAlignment textAlignment;
#end
CustomTextLayer.m
#import <CoreText/CoreText.h>
#import "CustomTextLayer.h"
#implementation CustomTextLayer
#synthesize text = _text, textColor = _textColor;
#synthesize font = _font, fontSize = _fontSize;
#synthesize strokeColor = _strokeColor, strokeWidth = _strokeWidth;
#synthesize textAlignment = _textAlignment;
-(id) init {
if (self = [super init]) {
_text = #"";
_textColor = [UIColor blackColor];
_font = #"Helvetica";
_fontSize = 12;
_strokeColor = [UIColor whiteColor];
_strokeWidth = 0.0;
_textAlignment = kCTLeftTextAlignment;
_lineBreakMode = kCTLineBreakByWordWrapping;
}
return self;
}
-(void) dealloc {
[_text release];
[_textColor release];
[_font release];
[_strokeColor release];
[super dealloc];
}
-(void) setText:(NSString *)text {
[_text release];
_text = [text retain];
[self setNeedsDisplay];
}
-(void) setTextColor:(UIColor *)textColor {
[_textColor release];
_textColor = [textColor retain];
[self setNeedsDisplay];
}
-(void) setFont:(NSString *)font {
[_font release];
_font = [font retain];
[self setNeedsDisplay];
}
-(void) setFontSize:(float)fontSize {
_fontSize = fontSize;
[self setNeedsDisplay];
}
-(void) setStrokeColor:(UIColor *)strokeColor {
[_strokeColor release];
_strokeColor = strokeColor;
[self setNeedsDisplay];
}
-(void) setStrokeWidth:(float)strokeWidth {
_strokeWidth = 0 ? (strokeWidth < 0) : (-1 * strokeWidth);
[self setNeedsDisplay];
}
-(void) setTextAlignment:(CTTextAlignment)textAlignment {
_textAlignment = textAlignment;
[self setNeedsDisplay];
}
-(void) setFrame:(CGRect)frame {
[super setFrame: frame];
[self setNeedsDisplay];
}
-(float) suggestedHeightForWidth:(float) width {
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)_font, _fontSize, NULL);
CTParagraphStyleSetting paragraphStyles[2] = {
{.spec = kCTParagraphStyleSpecifierLineBreakMode, .valueSize = sizeof(CTLineBreakMode), .value = (const void *) &_lineBreakMode},
{.spec = kCTParagraphStyleSpecifierAlignment, .valueSize = sizeof(CTTextAlignment), .value = (const void *) &_textAlignment}
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(paragraphStyles, 2);
NSDictionary *attrDict = [[NSDictionary alloc] initWithObjectsAndKeys:(id)fontRef, (NSString *)kCTFontAttributeName, (id)_textColor.CGColor, (NSString *)(kCTForegroundColorAttributeName), (id)_strokeColor.CGColor, (NSString *)(kCTStrokeColorAttributeName), (id)[NSNumber numberWithFloat: _strokeWidth], (NSString *)(kCTStrokeWidthAttributeName), (id)paragraphStyle, (NSString *)(kCTParagraphStyleAttributeName), nil];
CFRelease(fontRef);
CFRelease(paragraphStyle);
NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:_text attributes: attrDict];
// Determine suggested frame height
CFRange textRange = CFRangeMake(0, [attrStr length]);
CGSize constraint = CGSizeMake(width, 9999);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, textRange, NULL, constraint, NULL);
textSize = CGSizeMake(ceilf(textSize.width), ceilf(textSize.height));
[attrDict release];
[attrStr release];
return textSize.height;
}
-(void) renderText:(CGContextRef)ctx {
CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
CGContextTranslateCTM(ctx, 0, self.bounds.size.height);
CGContextScaleCTM(ctx, 1.0, -1.0);
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)_font, _fontSize, NULL);
CTParagraphStyleSetting paragraphStyles[2] = {
{.spec = kCTParagraphStyleSpecifierLineBreakMode, .valueSize = sizeof(CTLineBreakMode), .value = (const void *) &_lineBreakMode},
{.spec = kCTParagraphStyleSpecifierAlignment, .valueSize = sizeof(CTTextAlignment), .value = (const void *) &_textAlignment}
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(paragraphStyles, 2);
NSDictionary *attrDict = [[NSDictionary alloc] initWithObjectsAndKeys:(id)fontRef, (NSString *)kCTFontAttributeName, (id)_textColor.CGColor, (NSString *)(kCTForegroundColorAttributeName), (id)_strokeColor.CGColor, (NSString *)(kCTStrokeColorAttributeName), (id)[NSNumber numberWithFloat: _strokeWidth], (NSString *)(kCTStrokeWidthAttributeName), (id)paragraphStyle, (NSString *)(kCTParagraphStyleAttributeName), nil];
CFRelease(fontRef);
CFRelease(paragraphStyle);
NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:_text attributes: attrDict];
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CFRange textRange = CFRangeMake(0, [attrStr length]);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, textRange, path, NULL);
CFArrayRef lines = CTFrameGetLines(frame);
NSInteger numberOfLines = CFArrayGetCount(lines);
CGPoint lineOrigins[numberOfLines];
CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
CGPoint lineOrigin = lineOrigins[lineIndex];
CGContextSetTextPosition(ctx, lineOrigin.x, lineOrigin.y);
CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
if (lineIndex == numberOfLines - 1) {
CFRange lastLineRange = CTLineGetStringRange(line);
if (!(lastLineRange.length == 0 && lastLineRange.location == 0) && lastLineRange.location + lastLineRange.length < textRange.location + textRange.length) {
NSUInteger truncationAttributePosition = lastLineRange.location;
CTLineTruncationType truncationType;
if (numberOfLines != 1) {
truncationType = kCTLineTruncationEnd;
truncationAttributePosition += (lastLineRange.length - 1);
}
NSAttributedString *tokenString = [[NSAttributedString alloc] initWithString:#"\u2026" attributes:attrDict];
CTLineRef truncationToken = CTLineCreateWithAttributedString((CFAttributedStringRef)tokenString);
NSMutableAttributedString *truncationString = [[attrStr attributedSubstringFromRange: NSMakeRange(lastLineRange.location, lastLineRange.length)] mutableCopy];
if (lastLineRange.length > 0) {
unichar lastCharacter = [[truncationString string] characterAtIndex: lastLineRange.length - 1];
if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
[truncationString deleteCharactersInRange:NSMakeRange(lastLineRange.length - 1, 1)];
}
}
[truncationString appendAttributedString: tokenString];
CTLineRef truncationLine = CTLineCreateWithAttributedString((CFAttributedStringRef) truncationString);
CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, self.bounds.size.width, truncationType, truncationToken);
if (!truncatedLine) {
// If the line is not as wide as the truncationToken, truncatedLine is NULL
truncatedLine = CFRetain(truncationToken);
}
CTLineDraw(truncatedLine, ctx);
CFRelease(truncatedLine);
CFRelease(truncationLine);
CFRelease(truncationToken);
} else {
CTLineDraw(line, ctx);
}
} else {
CTLineDraw(line, ctx);
}
}
[attrStr release];
[attrDict release];
CFRelease(path);
CFRelease(frame);
CFRelease(framesetter);
}
-(void) drawInContext:(CGContextRef)ctx {
[super drawInContext: ctx];
[self renderText: ctx];
}
#end
I think to support both you can create a category for text layers, in category you can code it conditionally for both versions.
Same as we do for navigation bar when we change image.
You can center your frame as you did with different frames for different ios versions
It seems to me that iOS 6 has taken into account the Line Height (or other font related features that affects the actual vertical drawing position of the glyph) of the font when drawing the text contents of CATextLayer. The result is that in iOS 6.0, the text with certain font in CATextLayer is not displayed at the top edge of the frame of the CATextLayer. I found that some font has such vertical padding while others don't. While in iOS 5.0/5.1, the glyph of the text is actually displayed at the top edge of the frame of the CATextLayer.
So one possible solution I'm thinking may be to change the textLayer object in your code from CATextLayer to just CALayer (or subclass CALayer) and use Core Text to custom draw the contents such that you get to control of everything that will be consistent across iOS 5.0/5.1 and 6.0.

Resources