textkit custom text attribute draws big rect when it spans multiple lines - ios

I am basically creating a custom attribute to draw a rounded rectangle in my text subclassing NSLayoutManager with drawGlyphsForGlyphRange method below. Below works like a charm with ranges that spans one line. However, when the range of text spans in two lines, I am getting a big rectangle which draws the attribute along those two lines. I think I should be using a different approach here, I tried nsbackgroundattribute to draw the highlight but unfortunately I cannot make the highlight rounded rect using that.
I would appreciate any directions.
-(void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {
NSTextStorage *textStorage = self.textStorage;
NSRange glyphRange = glyphsToShow;
while (glyphRange.length > 0) {
NSRange charRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL], attributeCharRange, attributeGlyphRange;
id attribute = [textStorage attribute:IKSpecialHighlightAttributeName atIndex:charRange.location longestEffectiveRange:&attributeCharRange inRange:charRange];
attributeGlyphRange = [self glyphRangeForCharacterRange:attributeCharRange actualCharacterRange:NULL];
attributeGlyphRange = NSIntersectionRange(attributeGlyphRange, glyphRange);
if( attribute != nil ) {
NSTextContainer *textContainer = self.textContainers[0];
CGRect boundingRect = [self boundingRectForGlyphRange:attributeGlyphRange inTextContainer:textContainer];
[[UIColor colorWithRed:221.0/255.0 green:255.0/255.0 blue:0.0/255.0 alpha:1] setFill]; // set rounded rect's bg color
boundingRect.origin.x += origin.x-3.0;
boundingRect.origin.y += origin.y+3.0;
boundingRect.size.width += 6.0;
UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:boundingRect cornerRadius: 3.0];
[roundedRect fillWithBlendMode: kCGBlendModeNormal alpha:1.0f];
[super drawGlyphsForGlyphRange:attributeGlyphRange atPoint:origin];
}
else {
[super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin];
}
glyphRange.length = NSMaxRange(glyphRange) - NSMaxRange(attributeGlyphRange);
glyphRange.location = NSMaxRange(attributeGlyphRange);
}
}

Related

text labels drawn in CGRect form an elliptical arc when images drawn using the same coordinates form a circular arc

I am new to Core Graphics and trying to understand why text labels I draw in CGRect form an elliptical arc when images I draw using the same coordinates form a circular arc.
The original code by Arthur Knopper creates circles wherever the screen is touched. By removing the touches method, I have been able to generate a series of small circles (dots) along a circular arc (uber circle). Each dot is centred on the perimeter of the uber circle (as shown below).
In order to label each dot I use the same point coordinates I used for placing the dot. However text labels form an elliptical arc even though dots form a circular arc (as shown below). Labels are also hidden by the dots when dots are filled. The reason for this is a complete mystery.
As a novice I am probably missing something basic in Core Graphics. If anyone could explain what that is and what I need to do to make both arcs circular and place labels on top of the dots I’d be most grateful.
Thanks.
Here is the code.
circleView.h
NSMutableArray *totalCircles;
int dotCount, limit;
float uberX, uberY, uberRadius, uberAngle, labelX,
labelY,dotRadius, dotsFilled, sectors, x, y;
CGPoint dotPosition;
CGRect boxBoundary;
CGContextRef context;
}
- (void)demo;
#end
And ...
-#implementation iOSCircleView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code
totalCircles = [[NSMutableArray alloc] init];
// Set background color
self.backgroundColor = [UIColor whiteColor];
}
return self;
} // frame a view for drawing
- (void)drawRect:(CGRect)rect {
[self demo];
}
- (void)demo {
context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 0.5);
uberX = 120;
uberY = 160;
uberRadius = 30;
sectors = 16;
uberAngle = (2.0 * PI) / sectors;
dotRadius = 20;
dotsFilled = FALSE;
for (dotCount = 1; dotCount <= sectors; dotCount++)
{
// Create a new iOSCircle Object
iOSCircle *newCircle = [[iOSCircle alloc] init];
newCircle.circleRadius = dotRadius;
[self setSectorDotCoordinates]; // make new point for each dot
dotPosition = CGPointMake(x,y); // create each dot
NSLog(#"Circle%i: %#", dotCount, NSStringFromCGPoint(dotPosition));
[self autoLabel]; // text hides behind the dots
newCircle.circleCentre = dotPosition; // place each dot on the frame
[totalCircles addObject:newCircle];
[self setNeedsDisplay];
}
CGContextSetShadowWithColor(context, CGSizeMake(-3 , 2), 4.0, [UIColor clearColor].CGColor);
dotCount = 1;
for (iOSCircle *circle in totalCircles) {
CGContextAddArc(context, circle.circleCentre.x, circle.circleCentre.y, circle.circleRadius, 0.0, M_PI * 2.0, YES); // draw the circles
NSLog(#"Dot %i Filled %i ", dotCount, dotsFilled);
switch (dotsFilled) {
case 1:
CGContextSetFillColorWithColor(context, [[UIColor cyanColor] CGColor]);
//CGContextDrawPath(context, kCGPathFillStroke);
break;
default:
//CGContextStrokePath(context); // draw dot outline
break;
}
dotCount++;
}
} // draw circular dots in circular patterns
- (void)setSectorDotCoordinates {
x = uberX + (uberRadius * cos(uberAngle *dotCount) * 2);
y = uberY + (uberRadius * sin(uberAngle *dotCount) * 2);
} // calculate dot coordinates along a circular arc
- (void)autoLabel {
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
boxBoundary = CGRectMake(x-dotRadius, y-dotRadius, x+dotRadius, y+dotRadius);
[[NSString stringWithFormat:#"%i",dotCount] drawInRect:boxBoundary withFont:[UIFont systemFontOfSize:24] lineBreakMode:NSLineBreakByCharWrapping alignment:NSTextAlignmentCenter];
}
Change the boxBoundary in autoLabel, CGRectMake creates a rectangle with one point coordinates and width and height, not two points:
(void)autoLabel {
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
boxBoundary = CGRectMake(x-dotRadius, y-dotRadius, dotRadius*2, dotRadius*2);
[[NSString stringWithFormat:#"%i",dotCount] drawInRect:boxBoundary
withFont:[UIFont systemFontOfSize:24]
lineBreakMode:NSLineBreakByCharWrapping alignment:NSTextAlignmentCenter];
}
In your code the "boxes" containing the texts where bigger and bigger when you where going to the right. (the width and height were not fixed)
My updated code show labels that match the drawing order but text is still hidden when dots are filled.
I suspect I need to construct a path to write text in front of the dots and it’s already apparent that something like CGPathMoveToPoint is needed to start drawing from the 12 O'clock position.
Here’s the updated code. The first part draws and renders the dots
context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 0.5);
uberX = 160;
uberY = 240;
uberRadius = 52;
sectors = 16;
uberAngle = ((2.0 * PI) / sectors);
NSLog(#"%f %f %f %f", uberX, uberY, uberRadius, uberAngle);
dotRadius = 20;
dotsFilled = FALSE;
textOffset = 4; // add to y to centre the label
for (dotCount = 1; dotCount <= 4 /*sectors*/; dotCount++)
{
// Create a new iOSCircle Object
iOSCircle *newCircle = [[iOSCircle alloc] init];
newCircle.circleRadius = dotRadius;
[self setSectorDotCoordinates]; // create a new point for each dot
dotPosition = CGPointMake(x,y); // create each dot
NSLog(#"Circle%i: %#", dotCount, NSStringFromCGPoint(dotPosition));
newCircle.circleCentre = dotPosition; // place each dot on the frame
[totalCircles addObject:newCircle];
[self setNeedsDisplay];
}
CGContextSetShadowWithColor(context, CGSizeMake(-3 , 2), 4.0, [UIColor clearColor].CGColor);
dotCount = 1;
for (iOSCircle *circle in totalCircles) {
CGContextAddArc(context, circle.circleCentre.x, circle.circleCentre.y, circle.circleRadius, 0.0, M_PI * 2.0, YES);
// draw the circles
NSLog(#"Dot %i Filled %i ", dotCount, dotsFilled);
switch (dotsFilled) {
case 1:
CGContextSetFillColorWithColor(context, [[UIColor cyanColor] CGColor]);
CGContextDrawPath(context, kCGPathFillStroke);
break;
default:
CGContextStrokePath(context); // draw dot outline
break;
}
[self setSectorDotCoordinates]; // find point coordinates for each dot
dotCount++;
}
The code that draws the labels follow immediately afterwards.
// draw labels
for (dotCount = 1; dotCount <= sectors; dotCount++)
{
// Create a new iOSCircle Object
iOSCircle *newCircle = [[iOSCircle alloc] init];
newCircle.circleRadius = dotRadius;
[self setSectorDotCoordinates]; // find point coordinates for each dot
dotPosition = CGPointMake(x,y); // use point coordinates for label
[self autoLabel]; // THIS SHOWS TEXT BEHIND THE DOTS
}

How do I eliminate leading whitespace when computing word boundaries using boundingRectForGlyphRange in NSLayoutManager

I am decomposing a multiline string into word boundaries on iOS. My solution centers around the boundingRectForGlyphRange method of NSLayoutManager. It ALMOST works, except that the rect for each word is a few pixels off to the right. In other words NSLayoutManager seems to be adding a leading space / indent on each line and I cannot find any way to override this behavior.
I tried using NSLayoutManager.usesFontLeading as well as NSParagraphStyle.headIndent but without any results:
NSLayoutManager* layout = [NSLayoutManager new];
layout.usesFontLeading = NO;
NSMutableParagraphStyle* paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.headIndent = paragraphStyle.firstLineHeadIndent = 0;
NSTextStorage* textStorage = [[NSTextStorage alloc] initWithString:self attributes:#{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}];
layout.textStorage = textStorage;
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:size];
[layout addTextContainer:textContainer];
// compute bounding rect for each word range
for (NSValue* wordRangeValue in wordRanges)
{
NSRange wordRange = [wordRangeValue rangeValue];
NSRange wordGlyphRange = [layout glyphRangeForCharacterRange:wordRange actualCharacterRange:NULL];
CGRect wordBounds = [layout boundingRectForGlyphRange:wordGlyphRange inTextContainer:textContainer];
}
Screenshot: the gray rectangles represent label bounds, red rectangles represent text rect for each label and computed word boundaries from the [boundingRectForGlyphRange:] method above. Notice that the computed word boundaries are off by a few pixels.
I am also open to other methods for computing word boundaries, but boundingRectForGlyphRange seems very convenient for my purpose.
To ignore the left margin, use:
textContainer.lineFragmentPadding = 0;
I was not able to force NSLayoutManager to omit the left margin. If anyone knows how to get NSLayoutManager to ignore the left margin, let me know.
My workaround was to use Core Text instead. This was MUCH more difficult and involved. My solution does not lend itself to pasting into a single code excerpt, but this should give you a good reference if you want to go the same route:
- (NSArray*) coreTextLayoutsForCharacterRanges:(NSArray*)ranges withFont:(UIFont *)font constrainedToSize:(CGSize)size{
// initialization: make frame setter
NSMutableParagraphStyle* paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; // watch out - need to specify line wrapping to use multi line layout!
paragraphStyle.minimumLineHeight = font.lineHeight; // watch out - custom fonts do not compute properly without this!
NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:self attributes:#{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
CFRange wholeString = CFRangeMake(0, self.length);
CGRect bounds = CGRectMake(0, 0, size.width, size.height);
CGMutablePathRef boundsPath = CGPathCreateMutable();
CGPathAddRect(boundsPath, NULL, bounds);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, wholeString, boundsPath, NULL);
CFRelease(boundsPath);
// extract lines
CFArrayRef lines = CTFrameGetLines(frame);
int lineCount = CFArrayGetCount(lines);
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
NSMutableArray* lineLayouts = [NSMutableArray arrayWithCapacity:lineCount];
CGFloat h = size.height;
for (int i = 0; i < lineCount; ++i){
CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, i);
CGPoint lineOrigin = lineOrigins[i]; // in Core Graphics coordinates! let's convert.
lineOrigin.y = h - lineOrigin.y;
TextLayout* lineLayout = [[CTLineLayout alloc] initWithString:self line:line lineOrigin:lineOrigin];
[lineLayouts addObject:lineLayout];
}
// got line layouts. now we iterate through the word ranges to find the appropriate line for each word and compute its layout using the corresponding CTLine.
Another important part is how to get the bounding rect of a word in a line by using CTLine. I factored this into a CTLineLayout module, but the gist is this (the 'origin' variable refers to the line origin computed in the code sample above):
CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
CGFloat width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat top = origin.y - ascent;
CGFloat bottom = origin.y + descent;
CGRect result = CGRectMake(origin.x, top, width, bottom - top); // frame of entire line (for now)
CGFloat left = CTLineGetOffsetForStringIndex(line, stringRange.location, NULL);
CGFloat right = CTLineGetOffsetForStringIndex(line, NSMaxRange(stringRange), NULL);
result.origin.x = left;
result.size.width = right - left; // frame of target word in UIKit coordinates
The above is a rough excerpt - I factored CTLine to compute the line's bounds once in the initializer, then compute only the left + right endpoints when getting the frame of a word.
Whew!

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.

Drawing a path with subtracted text using Core Graphics

Creating filled paths in Core Graphics is straight-forward, as is creating filled text. But I am yet to find examples of paths filled EXCEPT for text in a sub-path. My experiments with text drawing modes, clipping etc have got me nowhere.
Here's an example (created in photoshop). How would you go about creating the foreground shape in Core Graphics?
I would mention that this technique appears to be used heavily in an upcoming version of a major mobile OS, but I don't want to fall afoul of SO's NDA-police ;)
Here's some code I ran and tested that will work for you. See the inline comments for details:
Update: I've removed the manualYOffset: parameter. It now does a calculation to center the text vertically in the circle. Enjoy!
- (void)drawRect:(CGRect)rect {
// Make sure the UIView's background is set to clear either in code or in a storyboard/nib
CGContextRef context = UIGraphicsGetCurrentContext();
[[UIColor whiteColor] setFill];
CGContextAddArc(context, CGRectGetMidX(rect), CGRectGetMidY(rect), CGRectGetWidth(rect)/2, 0, 2*M_PI, YES);
CGContextFillPath(context);
// Manual offset may need to be adjusted depending on the length of the text
[self drawSubtractedText:#"Foo" inRect:rect inContext:context];
}
- (void)drawSubtractedText:(NSString *)text inRect:(CGRect)rect inContext:(CGContextRef)context {
// Save context state to not affect other drawing operations
CGContextSaveGState(context);
// Magic blend mode
CGContextSetBlendMode(context, kCGBlendModeDestinationOut);
// This seemingly random value adjusts the text
// vertically so that it is centered in the circle.
CGFloat Y_OFFSET = -2 * (float)[text length] + 5;
// Context translation for label
CGFloat LABEL_SIDE = CGRectGetWidth(rect);
CGContextTranslateCTM(context, 0, CGRectGetHeight(rect)/2-LABEL_SIDE/2+Y_OFFSET);
// Label to center and adjust font automatically
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, LABEL_SIDE, LABEL_SIDE)];
label.font = [UIFont boldSystemFontOfSize:120];
label.adjustsFontSizeToFitWidth = YES;
label.text = text;
label.textAlignment = NSTextAlignmentCenter;
label.backgroundColor = [UIColor clearColor];
[label.layer drawInContext:context];
// Restore the state of other drawing operations
CGContextRestoreGState(context);
}
Here's the result (you can change the background to anything and you'll still be able to see through the text):
Below is a UIView subclass that will do what you want. It will correctly size and position 1 or more letters in the circle. Here's how it looks with 1-3 letters at various sizes (32, 64, 128, 256):
With the availability of user defined runtime attributes in Interface Builder, you can even configure the view from within IB. Just set the text property as a runtime attribute and the backgroundColor to the color you want for the circle.
Here's the code:
#interface MELetterCircleView : UIView
/**
* The text to display in the view. This should be limited to
* just a few characters.
*/
#property (nonatomic, strong) NSString *text;
#end
#interface MELetterCircleView ()
#property (nonatomic, strong) UIColor *circleColor;
#end
#implementation MELetterCircleView
- (instancetype)initWithFrame:(CGRect)frame text:(NSString *)text
{
NSParameterAssert(text);
self = [super initWithFrame:frame];
if (self)
{
self.text = text;
}
return self;
}
// Override to set the circle's background color.
// The view's background will always be clear.
-(void)setBackgroundColor:(UIColor *)backgroundColor
{
self.circleColor = backgroundColor;
[super setBackgroundColor:[UIColor clearColor]];
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
[self.circleColor setFill];
CGContextAddArc(context, CGRectGetMidX(rect), CGRectGetMidY(rect),
CGRectGetWidth(rect)/2, 0, 2*M_PI, YES);
CGContextFillPath(context);
[self drawSubtractedText:self.text inRect:rect inContext:context];
}
- (void)drawSubtractedText:(NSString *)text inRect:(CGRect)rect
inContext:(CGContextRef)context
{
CGContextSaveGState(context);
// Magic blend mode
CGContextSetBlendMode(context, kCGBlendModeDestinationOut);
CGFloat pointSize =
[self optimumFontSizeForFont:[UIFont boldSystemFontOfSize:100.f]
inRect:rect
withText:text];
UIFont *font = [UIFont boldSystemFontOfSize:pointSize];
// Move drawing start point for centering label.
CGContextTranslateCTM(context, 0,
(CGRectGetMidY(rect) - (font.lineHeight/2)));
CGRect frame = CGRectMake(0, 0, CGRectGetWidth(rect), font.lineHeight)];
UILabel *label = [[UILabel alloc] initWithFrame:frame];
label.font = font;
label.text = text;
label.textAlignment = NSTextAlignmentCenter;
label.backgroundColor = [UIColor clearColor];
[label.layer drawInContext:context];
// Restore the state of other drawing operations
CGContextRestoreGState(context);
}
-(CGFloat)optimumFontSizeForFont:(UIFont *)font inRect:(CGRect)rect
withText:(NSString *)text
{
// For current font point size, calculate points per pixel
CGFloat pointsPerPixel = font.lineHeight / font.pointSize;
// Scale up point size for the height of the label.
// This represents the optimum size of a single letter.
CGFloat desiredPointSize = rect.size.height * pointsPerPixel;
if ([text length] == 1)
{
// In the case of a single letter, we need to scale back a bit
// to take into account the circle curve.
// We could calculate the inner square of the circle,
// but this is a good approximation.
desiredPointSize = .80*desiredPointSize;
}
else
{
// More than a single letter. Let's make room for more.
desiredPointSize = desiredPointSize / [text length];
}
return desiredPointSize;
}
#end

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