Core Text and upper case numbers
Did you know there are such things as lower and upper case numbers? Upper case numbers are all the same height and sit on the baseline, while lower case numbers have ascenders and descenders. Some fonts support both, as in this example:
Switching between number case works differently in different programs. In TextEdit (or anything using MacOS X’s built in typography controls), you show the font panel, then click the little gear icon at the bottom left of it, then choose “Typography…”. You get a window like this:
The specific items in the window depend on what features your font supports. In this case, we’re looking at the “Number Case” option. Old-Style Figures are lower case numbers, and Lining Figures are upper case.
The reason I’m currently looking at number case is because I’m implementing an unread badge in an iOS app. I need to put a number centred in a circle, and our designer has given me the font to use. It turns out the font (Raleway, one of the Google fonts) defaults to lower case numbers, which makes centring them hard. The following two images show the label in exactly the same place, showing either 2 or 8 or both:
That’s not what we want: the number needs to be centred in the circle no matter which digit is displayed. We clearly need upper case numbers.
As far as I’m aware, UIFont has no way of setting upper case numbers. It was time to delve into the world of Core Text.
In Core Text language, the number case setting is called a feature. Let’s start by getting a list of all the possible features in our font:
CTFontRef fontFace = CTFontCreateWithName(CFSTR("Raleway-Bold"), 13.0, NULL);
CFArrayRef features = CTFontCopyFeatures(fontFace);
NSLog(@"%@", features);
This outputs a big structure listing lots of features, including this one:
{
CTFeatureTypeExclusive = 1;
CTFeatureTypeIdentifier = 21;
CTFeatureTypeName = "Number Case";
CTFeatureTypeNameID = "-2200";
CTFeatureTypeSelectors =
(
{
CTFeatureSelectorIdentifier = 0;
CTFeatureSelectorName = "Old-Style Figures";
CTFeatureSelectorNameID = "-2201";
},
{
CTFeatureSelectorIdentifier = 1;
CTFeatureSelectorName = "Lining Figures";
CTFeatureSelectorNameID = "-2202";
},
{
CTFeatureSelectorDefault = 1;
CTFeatureSelectorIdentifier = 2;
CTFeatureSelectorName = "No Change";
CTFeatureSelectorNameID = "-2203";
}
);
},
We can see that the values in here correspond to what MacOS X displays in the typography palette. The feature is called “Number Case”, and it has three options.
Here, we make a note of the CTFeatureTypeIdentifier and CTFeatureSelectorIdentifier that we care about: 21 and 1 respectively. Now we can make a CTFont with upper case numbers:
CTFontRef fontFace = CTFontCreateWithName(CFSTR("Raleway-Bold"), 13.0, NULL);
CTFontDescriptorRef prefontFace = CTFontCopyFontDescriptor(fontFace);
CTFontDescriptorRef modFace = CTFontDescriptorCreateCopyWithFeature(prefontFace,
(__bridge CFNumberRef)[NSNumber numberWithInt:21], // Number Case
(__bridge CFNumberRef)[NSNumber numberWithInt:1]); // Lining Figures
fontFace = CTFontCreateWithFontDescriptor(modFace, 13.0, NULL);
However, we’re not done yet. This is a CTFont rather than a UIFont, so we can’t display it in a UILabel. Also, there’s unfortunately no easy way of turning the former into the latter. The most common method people use is to interrogate the CTFont for its postscript name and size, then make a UIFont using that information. But if we did this here, we’d lose our upper case numbers.
Enter TTTAttributedLabel. It’s a replacement for UILabel that happens to render its content with Core Text. Perfect, right?
Not quite. TTTAttributedLabel still expects a UIFont when you do [label setFont:myFont]. Try to give it a CTFont and it’ll crash. But there is a workaround: TTTAttributedLabel can, as the name suggests, display attributed strings. It turns out that when it does, it can cope with CTFonts.
NSMutableParagraphStyle *paragraphstyle = [[NSMutableParagraphStyle alloc] init];
paragraphstyle.alignment = NSTextAlignmentCenter;
NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%i", unread] attributes:@{NSFontAttributeName : (__bridge id)fontFace,
NSForegroundColorAttributeName : [UIColor whiteColor],
NSParagraphStyleAttributeName : paragraphstyle}];
self.numUnreadLabel.text = attrStr;
I had to create a paragraph style in order to set the text alignment, since when you’re using an attributed string then TTTAttributedLabel seems to ignore the normal textAlignment setting.
Here’s what our badge looks like now (old on the left, new on the right):