Question

There seem to be other people with this problem, but I have yet to find a solution. I am trying to create a UILabel with a highlighted background, exactly like the labels underneath desktop files in OS X, as shown:

OS X desktop file label

I have decided to approach this using CoreText, and I have stumbled upon a bug I can't seem to solve:

The -drawRect Method

In my UILabel's drawRect method, I use CoreText to draw text to the screen, and then create a UIBezierPath for each line of text drawn. (The label will never have more than two lines)

CGContext context = UIGraphicsGetCurrentContext();
/* Flip coordinate plane */
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);

NSAttributedString *attributedText = [self attributedText]; /* [self attributedText] returns an NSAttributedString formatted with font, linebreak, and alignment. */

CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedText);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attributedText length]), path, NULL);
CFArrayRef lines = CTFrameGetLines(frame);

CGPoint *lineOrigins = malloc(CFArrayGetCount(lines));
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins); //<------

/* Get frame for line 1 */
CTLineRef line1 = CFArrayGetValueAtIndex(lines, 0);
CGRect lineFrame = CTLineGetImageBounds(line1, context); 

CGPoint lineFrameOrigin = CGPointMake(lineOrigins[0].x, self.bounds.size.height - lineOrigins[0].y);//Coordinates flipped once again
lineFrame.origin = lineFrameOrigin;

UIBezierPath *linePath = [UIBezierPath bezierPathWithRoundedRect:lineFrame cornerRadius:0];

[self.path appendPath:linePath];
/* Repeat for line 2 */

CTFrameDraw(frame, context);//Draw the text.

After the CGRect is obtained for each line, it is put into a UIBezierPath, which is then merged with other lines' path through the -appendPath method. A layer mask is made from the path, and the label's background color is changed to blue.

The Problem

Oddly enough, this is the output of the code above:

enter image description here

The mask is located too far down, and I'm not sure why. It appears that the CTFrameGetLineOrigins function is returning an incorrect y value.

By adding the following line of code:

lineFrameOrigin.y -= self.font.leading / 2;

The result is improved:

enter image description here

However, it still appears to be too far down on the y-axis. I believe that the above line is just a coincidence, a value that happens to be close to the correct y-value.

Anyway, as I know this question has been asked before, are there any alternative ways I can accomplish this? Has any new information appeared on this odd bug? I am dumbfounded by why the function's values are off.

Was it helpful?

Solution

I would do something like this to get a lines rect:

CTFrameRef frame;
CFArrayRef lines = CTFrameGetLines(frame);

CFIndex numLines = CFArrayGetCount(lines);

CTLineRef line1 = (CTLineRef)CFArrayGetValueAtIndex(lines, 0);

CGPoint *origins = (CGPoint *)malloc(numLines * sizeof(CGPoint));
CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), origins);

CGPoint origin = origins[0];
CGFloat ascent, descent, leading;

CTLineGetTypographicBounds(line1, &ascent, &descent, &leading);

CFRange lineRange = CTLineGetStringRange(line1);

CGFloat lineStart = CTLineGetOffsetForStringIndex(line1, lineRange.location, NULL);
CGFloat lineEnd   = CTLineGetOffsetForStringIndex(line1, CFRangeMaxRange(lineRange), NULL);

CGRect box = CGPathGetBoundingBox(CTFrameGetPath(frame));

CGFloat lineHeight = ascent + descent + leading;

CGFloat rectX = box.origin.x + origin.x + lineStart;
CGFloat rectY = box.origin.y + origin.y - lineHeight;

CGRect rectForLine = CGRectMake( rectX, rectY, lineEnd - lineStart, lineHeight);

I prefer getting typographical bounds over image bounds. I think you need the ascent, decent and leading to get the origin. I also always offset from the frames origin as well but you may not need to.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top