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 cod
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;
}