UIFontWeightTrait and UIFontDescriptorFamilyAttribute Ignored when creating UIFont from UIFontDescriptor

6.2k views Asked by At

Given the following code and a device running iOS 7.1 or later:

 NSDictionary *fontTraitsDictionary = @{UIFontWeightTrait : @(-1.0)};
 NSDictionary *attributesDictionary = @{
                                       UIFontDescriptorFamilyAttribute : @"Helvetica Neue", 
                                       UIFontDescriptorTraitsAttribute : fontTraitsDictionary
                                       };
 UIFontDescriptor *ultraLightDescriptor = [UIFontDescriptor fontDescriptorWithFontAttributes:attributesDictionary];
 UIFont *shouldBeAnUltraLightFont = [UIFont fontWithDescriptor:ultraLightDescriptor size:24];

 NSLog(@"%@", shouldBeAnUltraLightFont);

I would expect the value of shouldBeAnUltraLightFont to be an instance of HelveticaNeue-UltraLight, but instead it is:

<UICTFont: 0x908d160> font-family: "Helvetica"; font-weight: normal; font-style: normal; font-size: 24.00pt

I am following the Apple documentation as far as I understand it. Why is the font family and font weight information completely ignored?

Things I’ve Tried

  • I've tried other family names like Helvetica, Avenir, etc.
  • I've tried other font weights in the valid range from -1 to 1, in increments of 0.25

Regardless of these changes, the font returned is always a vanilla instance of Helvetica at normal weight.

5

There are 5 answers

1
Ellie P. On BEST ANSWER

I ran into the same issue, and the documentation was not much help. Eventually I figured out that using the family attribute combined with the face attribute worked:

 UIFontDescriptor* desc = [UIFontDescriptor fontDescriptorWithFontAttributes:
        @{
            UIFontDescriptorFamilyAttribute: @"Helvetica Neue",
            UIFontDescriptorFaceAttribute: @"Light"
        }
    ];
0
jawj On

When looking for a bold or italic font, symbolic traits at least do appear to be respected. So this works nicely:

+ (UIFont*)fontWithFamily:(NSString*)family bold:(BOOL)bold italic:(BOOL)italic size:(CGFloat)pointSize {
  UIFontDescriptorSymbolicTraits traits = 0;
  if (bold)   traits |= UIFontDescriptorTraitBold;
  if (italic) traits |= UIFontDescriptorTraitItalic;
  UIFontDescriptor* fd = [UIFontDescriptor
                          fontDescriptorWithFontAttributes:@{UIFontDescriptorFamilyAttribute: family,
                                                             UIFontDescriptorTraitsAttribute: @{UIFontSymbolicTrait:
                                                                                                  [NSNumber numberWithInteger:traits]}}];
  NSArray* matches = [fd matchingFontDescriptorsWithMandatoryKeys:
                      [NSSet setWithObjects:UIFontDescriptorFamilyAttribute, UIFontDescriptorTraitsAttribute, nil]];
  if (matches.count == 0) return nil;
  return [UIFont fontWithDescriptor:matches[0] size:pointSize];
}

e.g. [MyClass fontWithFamily:@"Avenir Next Condensed" bold:YES italic:NO size:12.0f];

I don't think this helps the OP, who was looking specifically for a light font with UIFontWeightTrait, but it may help others with similar problems.

1
robotspacer On

From the docs:

Font Traits Dictionary Keys

The following constants can be used as keys to retrieve information about a font descriptor from its trait dictionary.

NSString *const UIFontSymbolicTrait;
NSString *const UIFontWeightTrait;
NSString *const UIFontWidthTrait;
NSString *const UIFontSlantTrait;

This reads to me like these keys are designed only for getting information from a font descriptor—not for setting it. For example you could do something like this:

UIFont *font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute];
CGFloat weight = [traits[UIFontWeightTrait] floatValue];

Which would show you that the font you got back is a little bit heavier than the normal weight. This doesn't seem nearly as useful as being able to ask for a lighter weight of a font without having to give an exact name—but it seems to be the intended usage.

0
Amy Worrall On

This works for me:

  [maString
   enumerateAttribute:NSFontAttributeName
   inRange:<-- desired range -->
   options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
   usingBlock:^(id  _Nullable value, NSRange range, BOOL *_Nonnull stop) {
     if (![value isKindOfClass:[UIFont class]]) {
       return;
     }
     UIFontDescriptor *fontDescriptor = ((UIFont *)value).fontDescriptor;
     NSMutableDictionary *traits = [[fontDescriptor.fontAttributes objectForKey:UIFontDescriptorTraitsAttribute] mutableCopy] ?: [NSMutableDictionary new];
     traits[UIFontWeightTrait] = @(UIFontWeightSemibold);
     fontDescriptor = [fontDescriptor fontDescriptorByAddingAttributes:@{UIFontDescriptorTraitsAttribute : traits}];
     UIFont *semiboldFont = [UIFont fontWithDescriptor:fontDescriptor size:fontDescriptor.pointSize];
     if (semiboldFont) {
       [maString addAttribute:NSFontAttributeName value:semiboldFont range:range];
     }
   }];
0
msand On

You can use CTFontDescriptorCreateCopyWithVariation, but you have to find the variation axis identifier for the font weight first. I've implemented support for variable font weights in react-native-svg using this:

- (CTFontRef)getGlyphFont
{
    NSString *fontFamily = topFont_->fontFamily;
    NSNumber *fontSize = [NSNumber numberWithDouble:topFont_->fontSize];

    NSString *fontWeight = RNSVGFontWeightStrings[topFont_->fontWeight];
    NSString *fontStyle = RNSVGFontStyleStrings[topFont_->fontStyle];

    BOOL fontFamilyFound = NO;
    NSArray *supportedFontFamilyNames = [UIFont familyNames];

    if ([supportedFontFamilyNames containsObject:fontFamily]) {
        fontFamilyFound = YES;
    } else {
        for (NSString *fontFamilyName in supportedFontFamilyNames) {
            if ([[UIFont fontNamesForFamilyName: fontFamilyName] containsObject:fontFamily]) {
                fontFamilyFound = YES;
                break;
            }
        }
    }
    fontFamily = fontFamilyFound ? fontFamily : nil;

    UIFont *font = [RCTFont updateFont:nil
                              withFamily:fontFamily
                                    size:fontSize
                                  weight:fontWeight
                                   style:fontStyle
                                 variant:nil
                         scaleMultiplier:1.0];

    CTFontRef ref = (__bridge CTFontRef)font;

    int weight = topFont_->absoluteFontWeight;
    if (weight == 400) {
        return ref;
    }

    CFArrayRef cgAxes = CTFontCopyVariationAxes(ref);
    CFIndex cgAxisCount = CFArrayGetCount(cgAxes);
    CFNumberRef wght_id = 0;

    for (CFIndex i = 0; i < cgAxisCount; ++i) {
        CFTypeRef cgAxis = CFArrayGetValueAtIndex(cgAxes, i);
        if (CFGetTypeID(cgAxis) != CFDictionaryGetTypeID()) {
            continue;
        }

        CFDictionaryRef cgAxisDict = (CFDictionaryRef)cgAxis;
        CFTypeRef axisName = CFDictionaryGetValue(cgAxisDict, kCTFontVariationAxisNameKey);
        CFTypeRef axisId = CFDictionaryGetValue(cgAxisDict, kCTFontVariationAxisIdentifierKey);

        if (!axisName || CFGetTypeID(axisName) != CFStringGetTypeID()) {
            continue;
        }
        CFStringRef axisNameString = (CFStringRef)axisName;
        NSString *axisNameNSString = (__bridge NSString *)(axisNameString);
        if (![@"Weight" isEqualToString:axisNameNSString]) {
            continue;
        }

        if (!axisId || CFGetTypeID(axisId) != CFNumberGetTypeID()) {
            continue;
        }
        wght_id = (CFNumberRef)axisId;
        break;
    }

    if (wght_id == 0) {
        return ref;
    }
    UIFontDescriptor *uifd = font.fontDescriptor;
    CTFontDescriptorRef ctfd = (__bridge CTFontDescriptorRef)(uifd);
    CTFontDescriptorRef newfd = CTFontDescriptorCreateCopyWithVariation(ctfd, wght_id, (CGFloat)weight);
    CTFontRef newfont = CTFontCreateCopyWithAttributes(ref, (CGFloat)[fontSize doubleValue], nil, newfd);
    return newfont;
} 

https://github.com/react-native-community/react-native-svg/blob/bf0adb4a8206065ecb9e7cdaa18c3140d24ae338/ios/Text/RNSVGGlyphContext.m#L137-L235