Unwanted Vertical Padding from iOS 6 on CATextLayer - ios

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.

Related

Fit background color to special characters [duplicate]

I have a UILabel with space for two lines of text. Sometimes, when the text is too short, this text is displayed in the vertical center of the label.
How do I vertically align the text to always be at the top of the UILabel?
There's no way to set the vertical-align on a UILabel, but you can get the same effect by changing the label's frame. I've made my labels orange so you can see clearly what's happening.
Here's the quick and easy way to do this:
[myLabel sizeToFit];
If you have a label with longer text that will make more than one line, set numberOfLines to 0 (zero here means an unlimited number of lines).
myLabel.numberOfLines = 0;
[myLabel sizeToFit];
Longer Version
I'll make my label in code so that you can see what's going on. You can set up most of this in Interface Builder too. My setup is a View-Based App with a background image I made in Photoshop to show margins (20 points). The label is an attractive orange color so you can see what's going on with the dimensions.
- (void)viewDidLoad
{
[super viewDidLoad];
// 20 point top and left margin. Sized to leave 20 pt at right.
CGRect labelFrame = CGRectMake(20, 20, 280, 150);
UILabel *myLabel = [[UILabel alloc] initWithFrame:labelFrame];
[myLabel setBackgroundColor:[UIColor orangeColor]];
NSString *labelText = #"I am the very model of a modern Major-General, I've information vegetable, animal, and mineral";
[myLabel setText:labelText];
// Tell the label to use an unlimited number of lines
[myLabel setNumberOfLines:0];
[myLabel sizeToFit];
[self.view addSubview:myLabel];
}
Some limitations of using sizeToFit come into play with center- or right-aligned text. Here's what happens:
// myLabel.textAlignment = NSTextAlignmentRight;
myLabel.textAlignment = NSTextAlignmentCenter;
[myLabel setNumberOfLines:0];
[myLabel sizeToFit];
The label is still sized with a fixed top-left corner. You can save the original label's width in a variable and set it after sizeToFit, or give it a fixed width to counter these problems:
myLabel.textAlignment = NSTextAlignmentCenter;
[myLabel setNumberOfLines:0];
[myLabel sizeToFit];
CGRect myFrame = myLabel.frame;
// Resize the frame's width to 280 (320 - margins)
// width could also be myOriginalLabelFrame.size.width
myFrame = CGRectMake(myFrame.origin.x, myFrame.origin.y, 280, myFrame.size.height);
myLabel.frame = myFrame;
Note that sizeToFit will respect your initial label's minimum width. If you start with a label 100 wide and call sizeToFit on it, it will give you back a (possibly very tall) label with 100 (or a little less) width. You might want to set your label to the minimum width you want before resizing.
Some other things to note:
Whether lineBreakMode is respected depends on how it's set. NSLineBreakByTruncatingTail (the default) is ignored after sizeToFit, as are the other two truncation modes (head and middle). NSLineBreakByClipping is also ignored. NSLineBreakByCharWrapping works as usual. The frame width is still narrowed to fit to the rightmost letter.
Mark Amery gave a fix for NIBs and Storyboards using Auto Layout in the comments:
If your label is included in a nib or storyboard as a subview of the view of a ViewController that uses autolayout, then putting your sizeToFit call into viewDidLoad won't work, because autolayout sizes and positions the subviews after viewDidLoad is called and will immediately undo the effects of your sizeToFit call. However, calling sizeToFit from within viewDidLayoutSubviews will work.
My Original Answer (for posterity/reference):
This uses the NSString method sizeWithFont:constrainedToSize:lineBreakMode: to calculate the frame height needed to fit a string, then sets the origin and width.
Resize the frame for the label using the text you want to insert. That way you can accommodate any number of lines.
CGSize maximumSize = CGSizeMake(300, 9999);
NSString *dateString = #"The date today is January 1st, 1999";
UIFont *dateFont = [UIFont fontWithName:#"Helvetica" size:14];
CGSize dateStringSize = [dateString sizeWithFont:dateFont
constrainedToSize:maximumSize
lineBreakMode:self.dateLabel.lineBreakMode];
CGRect dateFrame = CGRectMake(10, 10, 300, dateStringSize.height);
self.dateLabel.frame = dateFrame;
Set the new text:
myLabel.text = #"Some Text"
Set the maximum number of lines to 0 (automatic):
myLabel.numberOfLines = 0
Set the frame of the label to the maximum size:
myLabel.frame = CGRectMake(20,20,200,800)
Call sizeToFit to reduce the frame size so the contents just fit:
[myLabel sizeToFit]
The labels frame is now just high and wide enough to fit your text. The top left should be unchanged. I have tested this only with the top left-aligned text. For other alignments, you might have to modify the frame afterward.
Also, my label has word wrapping enabled.
Refering to the extension solution:
for(int i=1; i< newLinesToPad; i++)
self.text = [self.text stringByAppendingString:#"\n"];
should be replaced by
for(int i=0; i<newLinesToPad; i++)
self.text = [self.text stringByAppendingString:#"\n "];
Additional space is needed in every added newline, because iPhone UILabels' trailing carriage returns seems to be ignored :(
Similarly, alignBottom should be updated too with a #" \n#%" in place of "\n#%" (for cycle initialization must be replaced by "for(int i=0..." too).
The following extension works for me:
// -- file: UILabel+VerticalAlign.h
#pragma mark VerticalAlign
#interface UILabel (VerticalAlign)
- (void)alignTop;
- (void)alignBottom;
#end
// -- file: UILabel+VerticalAlign.m
#implementation UILabel (VerticalAlign)
- (void)alignTop {
CGSize fontSize = [self.text sizeWithFont:self.font];
double finalHeight = fontSize.height * self.numberOfLines;
double finalWidth = self.frame.size.width; //expected width of label
CGSize theStringSize = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(finalWidth, finalHeight) lineBreakMode:self.lineBreakMode];
int newLinesToPad = (finalHeight - theStringSize.height) / fontSize.height;
for(int i=0; i<newLinesToPad; i++)
self.text = [self.text stringByAppendingString:#"\n "];
}
- (void)alignBottom {
CGSize fontSize = [self.text sizeWithFont:self.font];
double finalHeight = fontSize.height * self.numberOfLines;
double finalWidth = self.frame.size.width; //expected width of label
CGSize theStringSize = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(finalWidth, finalHeight) lineBreakMode:self.lineBreakMode];
int newLinesToPad = (finalHeight - theStringSize.height) / fontSize.height;
for(int i=0; i<newLinesToPad; i++)
self.text = [NSString stringWithFormat:#" \n%#",self.text];
}
#end
Then call [yourLabel alignTop]; or [yourLabel alignBottom]; after each yourLabel text assignment.
Just in case it's of any help to anyone, I had the same problem but was able to solve the issue simply by switching from using UILabel to using UITextView. I appreciate this isn't for everyone because the functionality is a bit different.
If you do switch to using UITextView, you can turn off all the Scroll View properties as well as User Interaction Enabled... This will force it to act more like a label.
No muss, no fuss
#interface MFTopAlignedLabel : UILabel
#end
#implementation MFTopAlignedLabel
- (void)drawTextInRect:(CGRect) rect
{
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:self.text attributes:#{NSFontAttributeName:self.font}];
rect.size.height = [attributedText boundingRectWithSize:rect.size
options:NSStringDrawingUsesLineFragmentOrigin
context:nil].size.height;
if (self.numberOfLines != 0) {
rect.size.height = MIN(rect.size.height, self.numberOfLines * self.font.lineHeight);
}
[super drawTextInRect:rect];
}
#end
No muss, no Objective-c, no fuss but Swift 3:
class VerticalTopAlignLabel: UILabel {
override func drawText(in rect:CGRect) {
guard let labelText = text else { return super.drawText(in: rect) }
let attributedText = NSAttributedString(string: labelText, attributes: [NSFontAttributeName: font])
var newRect = rect
newRect.size.height = attributedText.boundingRect(with: rect.size, options: .usesLineFragmentOrigin, context: nil).size.height
if numberOfLines != 0 {
newRect.size.height = min(newRect.size.height, CGFloat(numberOfLines) * font.lineHeight)
}
super.drawText(in: newRect)
}
}
Swift 4.2
class VerticalTopAlignLabel: UILabel {
override func drawText(in rect:CGRect) {
guard let labelText = text else { return super.drawText(in: rect) }
let attributedText = NSAttributedString(string: labelText, attributes: [NSAttributedString.Key.font: font])
var newRect = rect
newRect.size.height = attributedText.boundingRect(with: rect.size, options: .usesLineFragmentOrigin, context: nil).size.height
if numberOfLines != 0 {
newRect.size.height = min(newRect.size.height, CGFloat(numberOfLines) * font.lineHeight)
}
super.drawText(in: newRect)
}
}
Easiest approach using Storyboard:
Embed Label in a StackView and set the following two attributes of StackView in the Attribute Inspector:
1- Axis to Horizontal,
2- Alignment to Top
Like the answer above, but it wasn't quite right, or easy to slap into code so I cleaned it up a bit. Add this extension either to it's own .h and .m file or just paste right above the implementation you intend to use it:
#pragma mark VerticalAlign
#interface UILabel (VerticalAlign)
- (void)alignTop;
- (void)alignBottom;
#end
#implementation UILabel (VerticalAlign)
- (void)alignTop
{
CGSize fontSize = [self.text sizeWithFont:self.font];
double finalHeight = fontSize.height * self.numberOfLines;
double finalWidth = self.frame.size.width; //expected width of label
CGSize theStringSize = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(finalWidth, finalHeight) lineBreakMode:self.lineBreakMode];
int newLinesToPad = (finalHeight - theStringSize.height) / fontSize.height;
for(int i=0; i<= newLinesToPad; i++)
{
self.text = [self.text stringByAppendingString:#" \n"];
}
}
- (void)alignBottom
{
CGSize fontSize = [self.text sizeWithFont:self.font];
double finalHeight = fontSize.height * self.numberOfLines;
double finalWidth = self.frame.size.width; //expected width of label
CGSize theStringSize = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(finalWidth, finalHeight) lineBreakMode:self.lineBreakMode];
int newLinesToPad = (finalHeight - theStringSize.height) / fontSize.height;
for(int i=0; i< newLinesToPad; i++)
{
self.text = [NSString stringWithFormat:#" \n%#",self.text];
}
}
#end
And then to use, put your text into the label, and then call the appropriate method to align it:
[myLabel alignTop];
or
[myLabel alignBottom];
An even quicker (and dirtier) way to accomplish this is by setting the UILabel's line break mode to "Clip" and adding a fixed amount of newlines.
myLabel.lineBreakMode = UILineBreakModeClip;
myLabel.text = [displayString stringByAppendingString:"\n\n\n\n"];
This solution won't work for everyone -- in particular, if you still want to show "..." at the end of your string if it exceeds the number of lines you're showing, you'll need to use one of the longer bits of code -- but for a lot of cases this'll get you what you need.
Instead of UILabel you may use UITextField which has vertical alignment option:
textField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
textField.userInteractionEnabled = NO; // Don't allow interaction
I've struggled with this one for a long time and I wanted to share my solution.
This will give you a UILabel that will autoshrink text down to 0.5 scales and vertically center the text. These options are also available in Storyboard/IB.
[labelObject setMinimumScaleFactor:0.5];
[labelObject setBaselineAdjustment:UIBaselineAdjustmentAlignCenters];
Create a new class
LabelTopAlign
.h file
#import <UIKit/UIKit.h>
#interface KwLabelTopAlign : UILabel {
}
#end
.m file
#import "KwLabelTopAlign.h"
#implementation KwLabelTopAlign
- (void)drawTextInRect:(CGRect)rect {
int lineHeight = [#"IglL" sizeWithFont:self.font constrainedToSize:CGSizeMake(rect.size.width, 9999.0f)].height;
if(rect.size.height >= lineHeight) {
int textHeight = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(rect.size.width, rect.size.height)].height;
int yMax = textHeight;
if (self.numberOfLines > 0) {
yMax = MIN(lineHeight*self.numberOfLines, yMax);
}
[super drawTextInRect:CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, yMax)];
}
}
#end
Edit
Here's a simpler implementation that does the same:
#import "KwLabelTopAlign.h"
#implementation KwLabelTopAlign
- (void)drawTextInRect:(CGRect)rect
{
CGFloat height = [self.text sizeWithFont:self.font
constrainedToSize:rect.size
lineBreakMode:self.lineBreakMode].height;
if (self.numberOfLines != 0) {
height = MIN(height, self.font.lineHeight * self.numberOfLines);
}
rect.size.height = MIN(rect.size.height, height);
[super drawTextInRect:rect];
}
#end
In Interface Builder
Set UILabel to size of biggest possible Text
Set Lines to '0' in Attributes Inspector
In your code
Set the text of the label
Call sizeToFit on your label
Code Snippet:
self.myLabel.text = #"Short Title";
[self.myLabel sizeToFit];
For Adaptive UI(iOS8 or after) , Vertical Alignment of UILabel is to be set from StoryBoard by Changing the properties
noOfLines=0` and
Constraints
Adjusting UILabel LefMargin, RightMargin and Top Margin Constraints.
Change Content Compression Resistance Priority For Vertical=1000` So that Vertical>Horizontal .
Edited:
noOfLines=0
and the following constraints are enough to achieve the desired results.
Create a subclass of UILabel. Works like a charm:
// TopLeftLabel.h
#import <Foundation/Foundation.h>
#interface TopLeftLabel : UILabel
{
}
#end
// TopLeftLabel.m
#import "TopLeftLabel.h"
#implementation TopLeftLabel
- (id)initWithFrame:(CGRect)frame
{
return [super initWithFrame:frame];
}
- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines
{
CGRect textRect = [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines];
textRect.origin.y = bounds.origin.y;
return textRect;
}
-(void)drawTextInRect:(CGRect)requestedRect
{
CGRect actualRect = [self textRectForBounds:requestedRect limitedToNumberOfLines:self.numberOfLines];
[super drawTextInRect:actualRect];
}
#end
As discussed here.
What I did in my app was to set the UILabel's line property to 0 as well as to create a bottom constraint of the UILabel and make sure it is being set to >= 0 as shown in the image below.
Use textRect(forBounds:limitedToNumberOfLines:).
class TopAlignedLabel: UILabel {
override func drawText(in rect: CGRect) {
let textRect = super.textRect(forBounds: bounds, limitedToNumberOfLines: numberOfLines)
super.drawText(in: textRect)
}
}
I wrote a util function to achieve this purpose. You can take a look:
// adjust the height of a multi-line label to make it align vertical with top
+ (void) alignLabelWithTop:(UILabel *)label {
CGSize maxSize = CGSizeMake(label.frame.size.width, 999);
label.adjustsFontSizeToFitWidth = NO;
// get actual height
CGSize actualSize = [label.text sizeWithFont:label.font constrainedToSize:maxSize lineBreakMode:label.lineBreakMode];
CGRect rect = label.frame;
rect.size.height = actualSize.height;
label.frame = rect;
}
.How to use? (If lblHello is created by Interface builder, so I skip some UILabel attributes detail)
lblHello.text = #"Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!";
lblHello.numberOfLines = 5;
[Utils alignLabelWithTop:lblHello];
I also wrote it on my blog as an article:
http://fstoke.me/blog/?p=2819
I took a while to read the code, as well as the code in the introduced page, and found that they all try to modify the frame size of label, so that the default center vertical alignment would not appear.
However, in some cases we do want the label to occupy all those spaces, even if the label does have so much text (e.g. multiple rows with equal height).
Here, I used an alternative way to solve it, by simply pad newlines to the end of label (pls note that I actually inherited the UILabel, but it is not necessary):
CGSize fontSize = [self.text sizeWithFont:self.font];
finalHeight = fontSize.height * self.numberOfLines;
finalWidth = size.width; //expected width of label
CGSize theStringSize = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(finalWidth, finalHeight) lineBreakMode:self.lineBreakMode];
int newLinesToPad = (finalHeight - theStringSize.height) / fontSize.height;
for(int i = 0; i < newLinesToPad; i++)
{
self.text = [self.text stringByAppendingString:#"\n "];
}
I took the suggestions here and created a view which can wrap a UILabel and will size it and set the number of lines so that it is top aligned. Simply put a UILabel as a subview:
#interface TopAlignedLabelContainer : UIView
{
}
#end
#implementation TopAlignedLabelContainer
- (void)layoutSubviews
{
CGRect bounds = self.bounds;
for (UILabel *label in [self subviews])
{
if ([label isKindOfClass:[UILabel class]])
{
CGSize fontSize = [label.text sizeWithFont:label.font];
CGSize textSize = [label.text sizeWithFont:label.font
constrainedToSize:bounds.size
lineBreakMode:label.lineBreakMode];
label.numberOfLines = textSize.height / fontSize.height;
label.frame = CGRectMake(0, 0, textSize.width,
fontSize.height * label.numberOfLines);
}
}
}
#end
You can use TTTAttributedLabel, it supports vertical alignment.
#property (nonatomic) TTTAttributedLabel* label;
<...>
//view's or viewController's init method
_label.verticalAlignment = TTTAttributedLabelVerticalAlignmentTop;
I've found the answers on this question are now a bit out-of-date, so adding this for the auto layout fans out there.
Auto layout makes this issue pretty trivial. Assuming we're adding the label to UIView *view, the following code will accomplish this:
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
[label setText:#"Some text here"];
[label setTranslatesAutoresizingMaskIntoConstraints:NO];
[view addSubview:label];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[label]|" options:0 metrics:nil views:#{#"label": label}]];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[label]" options:0 metrics:nil views:#{#"label": label}]];
The label's height will be calculated automatically (using it's intrinsicContentSize) and the label will be positioned edge-to-edge horizontally, at the top of the view.
I've used a lot of the methods above, and just want to add a quick-and-dirty approach I've used:
myLabel.text = [NSString stringWithFormat:#"%#\n\n\n\n\n\n\n\n\n",#"My label text string"];
Make sure the number of newlines in the string will cause any text to fill the available vertical space, and set the UILabel to truncate any overflowing text.
Because sometimes good enough is good enough.
I wanted to have a label which was able to have multi-lines, a minimum font size, and centred both horizontally and vertically in it's parent view. I added my label programmatically to my view:
- (void) customInit {
// Setup label
self.label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
self.label.numberOfLines = 0;
self.label.lineBreakMode = UILineBreakModeWordWrap;
self.label.textAlignment = UITextAlignmentCenter;
// Add the label as a subview
self.autoresizesSubviews = YES;
[self addSubview:self.label];
}
And then when I wanted to change the text of my label...
- (void) updateDisplay:(NSString *)text {
if (![text isEqualToString:self.label.text]) {
// Calculate the font size to use (save to label's font)
CGSize textConstrainedSize = CGSizeMake(self.frame.size.width, INT_MAX);
self.label.font = [UIFont systemFontOfSize:TICKER_FONT_SIZE];
CGSize textSize = [text sizeWithFont:self.label.font constrainedToSize:textConstrainedSize];
while (textSize.height > self.frame.size.height && self.label.font.pointSize > TICKER_MINIMUM_FONT_SIZE) {
self.label.font = [UIFont systemFontOfSize:self.label.font.pointSize-1];
textSize = [ticker.blurb sizeWithFont:self.label.font constrainedToSize:textConstrainedSize];
}
// In cases where the frame is still too large (when we're exceeding minimum font size),
// use the views size
if (textSize.height > self.frame.size.height) {
textSize = [text sizeWithFont:self.label.font constrainedToSize:self.frame.size];
}
// Draw
self.label.frame = CGRectMake(0, self.frame.size.height/2 - textSize.height/2, self.frame.size.width, textSize.height);
self.label.text = text;
}
[self setNeedsDisplay];
}
Hope that helps someone!
FXLabel (on github) does this out of the box by setting label.contentMode to UIViewContentModeTop. This component is not made by me, but it is a component I use frequently and has tons of features, and seems to work well.
for anyone reading this because the text inside your label is not vertically centered, keep in mind that some font types are not designed equally. for example, if you create a label with zapfino size 16, you will see the text is not perfectly centered vertically.
however, working with helvetica will vertically center your text.
Subclass UILabel and constrain the drawing rectangle, like this:
- (void)drawTextInRect:(CGRect)rect
{
CGSize sizeThatFits = [self sizeThatFits:rect.size];
rect.size.height = MIN(rect.size.height, sizeThatFits.height);
[super drawTextInRect:rect];
}
I tried the solution involving newline padding and ran into incorrect behavior in some cases. In my experience, it's easier to constrain the drawing rect as above than mess with numberOfLines.
P.S. You can imagine easily supporting UIViewContentMode this way:
- (void)drawTextInRect:(CGRect)rect
{
CGSize sizeThatFits = [self sizeThatFits:rect.size];
if (self.contentMode == UIViewContentModeTop) {
rect.size.height = MIN(rect.size.height, sizeThatFits.height);
}
else if (self.contentMode == UIViewContentModeBottom) {
rect.origin.y = MAX(0, rect.size.height - sizeThatFits.height);
rect.size.height = MIN(rect.size.height, sizeThatFits.height);
}
[super drawTextInRect:rect];
}
If you are using autolayout, set the vertical contentHuggingPriority to 1000, either in code or IB. In IB you may then have to remove a height constraint by setting it's priority to 1 and then deleting it.
As long as you are not doing any complex task, you can use UITextView instead of UILabels.
Disable the scroll.
If you want the text to be displayed completely just user sizeToFit and sizeThatFits: methods
In swift,
let myLabel : UILabel!
To make your text of your Label to fit to screen and it's on the top
myLabel.sizeToFit()
To make your font of label to fit to the width of screen or specific width size.
myLabel.adjustsFontSizeToFitWidth = YES
and some textAlignment for label :
myLabel.textAlignment = .center
myLabel.textAlignment = .left
myLabel.textAlignment = .right
myLabel.textAlignment = .Natural
myLabel.textAlignment = .Justified
This is an old solution, use the autolayout on iOS >= 6
My solution:
Split lines by myself (ignoring label wrap settings)
Draw lines by myself (ignoring label alignment)
#interface UITopAlignedLabel : UILabel
#end
#implementation UITopAlignedLabel
#pragma mark Instance methods
- (NSArray*)splitTextToLines:(NSUInteger)maxLines {
float width = self.frame.size.width;
NSArray* words = [self.text componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSMutableArray* lines = [NSMutableArray array];
NSMutableString* buffer = [NSMutableString string];
NSMutableString* currentLine = [NSMutableString string];
for (NSString* word in words) {
if ([buffer length] > 0) {
[buffer appendString:#" "];
}
[buffer appendString:word];
if (maxLines > 0 && [lines count] == maxLines - 1) {
[currentLine setString:buffer];
continue;
}
float bufferWidth = [buffer sizeWithFont:self.font].width;
if (bufferWidth < width) {
[currentLine setString:buffer];
}
else {
[lines addObject:[NSString stringWithString:currentLine]];
[buffer setString:word];
[currentLine setString:buffer];
}
}
if ([currentLine length] > 0) {
[lines addObject:[NSString stringWithString:currentLine]];
}
return lines;
}
- (void)drawRect:(CGRect)rect {
if ([self.text length] == 0) {
return;
}
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, self.textColor.CGColor);
CGContextSetShadowWithColor(context, self.shadowOffset, 0.0f, self.shadowColor.CGColor);
NSArray* lines = [self splitTextToLines:self.numberOfLines];
NSUInteger numLines = [lines count];
CGSize size = self.frame.size;
CGPoint origin = CGPointMake(0.0f, 0.0f);
for (NSUInteger i = 0; i < numLines; i++) {
NSString* line = [lines objectAtIndex:i];
if (i == numLines - 1) {
[line drawAtPoint:origin forWidth:size.width withFont:self.font lineBreakMode:UILineBreakModeTailTruncation];
}
else {
[line drawAtPoint:origin forWidth:size.width withFont:self.font lineBreakMode:UILineBreakModeClip];
}
origin.y += self.font.lineHeight;
if (origin.y >= size.height) {
return;
}
}
}
#end

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?

How to set max height and fixed width for UITextView and force ellipsis rather than scroll if overflow occurs?

Here's my goal:
I want to use a UITextView rather than a UILabel because I want users to be able to select text and copy.
I want the UITextView to max out at a height of 60 points.
I want the UITextView to have a fixed width of 300 points.
I want the UITextView to line break on words.
Let's say, based on the attributed text string I feed it, that it takes 3 lines to reach the 60 point max height. Therefore, if I feed the UITextView 6 lines worth of attributed text I want the UITextView to max out at 60 points and display 3 lines followed by an ellipsis (e.g. ...).
I don't want the text view to ever be scrollable.
If I feed the UITextView a single word as attributed text, such as "Hello", I want the UITextView to still have a fixed width of 300 points but a dynamic height that scales to as small as it can be, approximately 20 points for a single line of text in this example.
I want the UITextView to have zero internal padding.
Any ideas?
- (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andWidth:(CGFloat)width
{
UITextView *calculationView = [[UITextView alloc] init];
[calculationView setAttributedText:text];
CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return size.height;
}
- (void)viewDidLoad
{
// Invoke super
[super viewDidLoad];
// Get text of unknown length
NSMutableAttributedString *myAttributedString = [[NSMutableAttributedString alloc] initWithString:#"String of unknown length here..." attributes:#{NSForegroundColorAttributeName : [UIColor redColor]}];
// Get ellipsis w/ matching attributes
NSDictionary *endCharAttributes = [myAttributedString attributesAtIndex:myAttributedString.length - 1 effectiveRange:NULL];
NSAttributedString *ellipsis = [[NSAttributedString alloc] initWithString:#"..." attributes:endCharAttributes];
// Define size constraints
CGFloat maxHeight = 60;
CGFloat fixedWidth = 300;
// Get starting height
CGFloat textViewHeight = [self textViewHeightForAttributedText:myAttributedString andWidth:fixedWidth];
// Reduce string size and add ellipsis until we fit within our height constraint
while (textViewHeight > maxHeight)
{
NSLog(#"%f", textViewHeight);
NSRange substringRange = {0, myAttributedString.length - 6}; // Reducing by 6 works for my app (strings are never huge)
myAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:[myAttributedString attributedSubstringFromRange:substringRange]];
[myAttributedString appendAttributedString:ellipsis];
NSLog(#"myAttributedString = %#", myAttributedString);
textViewHeight = [self textViewHeightForAttributedText:myAttributedString andWidth:fixedWidth];
}
// Init and config UITextView
UITextView *textView = [[UITextView alloc] init];
textView.attributedText = myAttributedString;
textView.frame = CGRectMake(0, 0, fixedWidth, textViewHeight);
[self.view addSubview:textView];
}
Have a more elegant solution? Post it!
UPDATE: You can increase the performance of - (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andWidth:(CGFloat)width by adding a helpers class and implementing the the following class methods:
// Private, gets us to alloc init calculation view one time for life of application
+ (UITextView *)calculationView
{
static UITextView *_calculationView;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_calculationView = [[UITextView alloc] init];
});
return _calculationView;
}
// Public, app calls this a lot
+ (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andWidth:(CGFloat)width usingUIEdgeInset:(UIEdgeInsets)edgeInsets
{
[self calculationView].textContainerInset = edgeInsets;
[self calculationView].attributedText = text;
CGSize size = [[self calculationView] sizeThatFits:CGSizeMake(width, FLT_MAX)];
return size.height;
}
Have you considered subclassing UILabel and adding the capability to select and copy text? Mattt Thompson has a good example here.
My solution is
- (NSString*)stringByTruncatingToWidth:(CGFloat)width maxHeight:(CGFloat)maxHeight font:(UIFont *)font;
{
NSString *ellipsis = #"…";
NSMutableString *truncatedString = [self mutableCopy];
if ([self textSizeForMaxWidth:width font:font].height > maxHeight)
{
truncatedString = [[truncatedString stringByAppendingString:ellipsis] mutableCopy];
NSRange range = {truncatedString.length - 4, 1};
[truncatedString deleteCharactersInRange:range];
while ([truncatedString textSizeForMaxWidth:width font:font].height > maxHeight){
[truncatedString deleteCharactersInRange:range];
range.location--;
}
}
return truncatedString;
}
And help method for calculating text size for max width
- (CGSize)textSizeForMaxWidth:(CGFloat)width font:(UIFont *)font
{
NSTextStorage *textStorage = [[NSTextStorage alloc]
initWithString:self];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize: CGSizeMake(width, MAXFLOAT)];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
[textStorage addAttribute:NSFontAttributeName value:font
range:NSMakeRange(0, [textStorage length])];
[textContainer setLineFragmentPadding:0.0];
[layoutManager glyphRangeForTextContainer:textContainer];
CGRect frame = [layoutManager usedRectForTextContainer:textContainer];
return frame.size;
}

How to resize text to label size?

I want to stretch the text inside a UILabel so that if fits exactly into the label (both width and height). I don't want to resize the UILabel in any way.
So far i used this: How to render stretched text in iOS? , but the text doesn't stretch 100% ( sometimes it exceeds the boundaries, and sometimes it leaves spacing on the margins ).
Is there another (preferably easier) way to do this?
This is what i was talking about: http://i.imgur.com/AMvfhsA.png . I get spacing on the left and the text exceeds boundaries on the right and also on the bottom edge.
This is the custom label class:
#import "CustomUILabel.h"
#implementation CustomUILabel
- (id)initWithFrame:(CGRect)frame text:(NSString*)text
{
self = [super initWithFrame:frame];
if (self) {
self.edgeInsets = UIEdgeInsetsMake(0, 0, 0, 0);
self.text = text;
}
return self;
}
- (void)drawTextInRect:(CGRect)rect {
[super drawTextInRect:UIEdgeInsetsInsetRect(rect, self.edgeInsets)];
}
- (void)drawRect:(CGRect)rect
{
[self drawScaledString:self.text];
}
- (void)drawScaledString:(NSString *)string
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
NSAttributedString *attrString = [self generateAttributedString:string];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attrString, CFRangeMake(0, string.length),
kCTForegroundColorAttributeName, self.textColor.CGColor);
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef) attrString);
// CTLineGetTypographicBounds doesn't give correct values,
// using GetImageBounds instead
CGRect imageBounds = CTLineGetImageBounds(line, context);
CGFloat width = imageBounds.size.width;
CGFloat height = imageBounds.size.height;
CGFloat padding = 0;
width += padding;
height += padding;
float sx = self.bounds.size.width / width;
float sy = self.bounds.size.height / height;
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 1, self.bounds.size.height);
CGContextScaleCTM(context, 1, -1);
CGContextScaleCTM(context, sx, sy);
CGContextSetTextPosition(context, -imageBounds.origin.x + padding/2, -imageBounds.origin.y + padding/2);
CTLineDraw(line, context);
CFRelease(line);
}
- (NSAttributedString *)generateAttributedString:(NSString *)string
{
CTFontRef helv = CTFontCreateWithName(CFSTR("Helvetica-Bold"),20, NULL);
CGColorRef color = [UIColor blackColor].CGColor;
NSDictionary *attributesDict = [NSDictionary dictionaryWithObjectsAndKeys:
(__bridge id)helv, (NSString *)kCTFontAttributeName,
color, (NSString *)kCTForegroundColorAttributeName,
nil];
NSAttributedString *attrString = [[NSMutableAttributedString alloc]
initWithString:string
attributes:attributesDict];
return attrString;
}
#end
And this is how i use it (I've added the label from storyboards):
#property (weak, nonatomic) IBOutlet CustomUILabel *label;
...
self.label.backgroundColor = [UIColor redColor];
self.label.text = #"OOOOO";
What you're asking boils down to calculating the exact bounding box of rendered text. When you have this box you can adjust the CTM to make the text fill the desired area.
Yet: There does not seem to be an easy way to do this. A lot of issues contribute to this problem:
Font metrics are a pretty complex topic. A rendered character (a glyph) has several bounding boxes depending on the intention.
In fonts, glyphs are often represented using bezier curves which makes calculating an exact bounding box difficult.
Attributes might influence graphical appearance in unforeseeable ways. AppKit/UIKit for example know a shadow attribute that can extend the area in which the font renders pixels.
There are more issues but the ones I listed might be enough to show that the task of exactly filling a box with a rendered text is not so easy.
Maybe there's another way of doing what you have in mind.

Underline text in UIlabel

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])
}
}
}

Resources