Unbolding a UIFontDescriptor

Brian Nickel picture Brian Nickel · Nov 6, 2013 · Viewed 7.2k times · Source

I'm using dynamic type in an application and have scenarios where I want to change the font's appearance, for example making it italic or unbolding it. Adding a style is easy enough:

UIFontDescriptor *descriptor = [[UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline]
                                fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitItalic];
UIFont *font = [UIFont fontWithDescriptor:descriptor size:descriptor.pointSize];

There's no clear mechanism for removing a style however. I could try adjusting the attributes but they look even more daunting, with completely undocumented API's:

Regular Headline: {
    NSCTFontUIUsageAttribute = UICTFontTextStyleHeadline;
    NSFontNameAttribute = ".AppleSystemUIHeadline";
    NSFontSizeAttribute = 17;
}

Italic Headline: {
    NSCTFontUIUsageAttribute = UICTFontTextStyleItalicHeadline;
    NSFontNameAttribute = ".AppleSystemUIItalicHeadline";
    NSFontSizeAttribute = 17;
}

Is there another avenue I'm missing? I could use [UIFont systemFontWithSize:descriptor.pointSize] but I don't want to lose whatever drawing rules are provided by dynamic type.

Answer

ipaterson picture ipaterson · Nov 8, 2013

The fontDescriptorWithSymbolicTraits: method is actually capable of doing what you want, with the exception of some edge cases in font trait support among the built-in semantic text styles. The key concept here is that this method replaces all symbolic traits on the previous descriptor with the new trait(s). The documentation is a bit wishy-washy on this simply stating that the new traits "take precedence over" the old.

Bitwise operations are used to add and remove specific traits, but it appears that special care is required when working with a descriptor generated by preferredFontDescriptorWithTextStyle:. Not all fonts support all traits. The headline font, for instance, is weighted according to the user's preferred content size and even if you can strip the descriptor of its bold trait, the matching UIFont will be bold. Unfortunately, this is not documented anywhere so the discovery of any additional nuances is left as an exercise for the reader.

The following example illustrates these issues:

// Start with a system font, in this case the headline font
// bold: YES italic: NO
UIFontDescriptor * originalDescriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline];
NSLog(@"originalDescriptor bold: %d italic: %d",
      isBold(originalDescriptor), isItalic(originalDescriptor));

// Try to set the italic trait. This may not be what you expected; the 
// italic trait is not added. On a normal UIFontDescriptor the italic
// trait would have been set and the bold trait unset.
// Ultimately it seems that there is no variant of the headline font that
// is italic but not bold.
// bold: YES italic: NO
UIFontDescriptor * italicDescriptor = [originalDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitItalic];
NSLog(@"italicDescriptor bold: %d italic: %d",
      isBold(italicDescriptor), isItalic(italicDescriptor));

// The correct way to make this font descriptor italic (and coincidentally
// the safe way to make any other descriptor italic without discarding its
// other traits) would be as follows:
// bold: YES italic: YES
UIFontDescriptor * boldItalicDescriptor = [originalDescriptor fontDescriptorWithSymbolicTraits:(originalDescriptor.symbolicTraits | UIFontDescriptorTraitItalic)];
NSLog(@"boldItalicDescriptor bold: %d italic: %d",
      isBold(boldItalicDescriptor), isItalic(boldItalicDescriptor));

// Your intention was to remove bold without affecting any other traits, which
// is also easy to do with bitwise logic.
// Using the originalDescriptor, remove bold by negating it then applying
// a logical AND to filter it out of the existing traits.
// bold: NO  italic: NO
UIFontDescriptor * nonBoldDescriptor = [originalDescriptor fontDescriptorWithSymbolicTraits:(originalDescriptor.symbolicTraits & ~UIFontDescriptorTraitBold)];
NSLog(@"nonBoldDescriptor bold: %d italic: %d",
      isBold(nonBoldDescriptor), isItalic(nonBoldDescriptor));

// Seems like it worked, EXCEPT there is no font that matches. Turns out
// there is no regular weight alternative for the headline style font.
// To confirm, test with UIFontDescriptorTraitsAttribute as the mandatory
// key and you'll get back a nil descriptor.
// bold: YES italic: NO
nonBoldDescriptor = [nonBoldDescriptor matchingFontDescriptorsWithMandatoryKeys:nil].firstObject;
NSLog(@"nonBoldDescriptor bold: %d italic: %d",
      isBold(nonBoldDescriptor), isItalic(nonBoldDescriptor));

FYI, the isBold and isItalic functions used above for the sake of brevity could be implemented as follows:

 BOOL isBold(UIFontDescriptor * fontDescriptor)
 {
    return (fontDescriptor.symbolicTraits & UIFontDescriptorTraitBold) != 0;
 }

 BOOL isItalic(UIFontDescriptor * fontDescriptor)
 {
    return (fontDescriptor.symbolicTraits & UIFontDescriptorTraitItalic) != 0;
 }