21
votes

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
If you remove the UIFontDescriptorTraitsAttribute key from the dictionary, then the resulting font is of the correct family ("Helvetica Neue", not "Helvetica"). Probably a bug. It seems more likely that you should be using fontDescriptorWithSymbolicTraits to get the correct font, but UIFontDescriptorSymbolicTraits is lacking a value for light/ultralight. Probably an oversight.blork
On 8.0b4 Helvetica Neue is returned (<UICTFont: 0x7b22c690> font-family: "Helvetica Neue"; font-weight: normal; font-style: normal; font-size: 24.00pt), so there's some progress.Arek Holko
To me it looks like there's nothing wrong with your code, but rather with the implementation of the fonts in iOS. If each of the variations of Helvetica Neue is actually its own font with normal weight, looking for an lightweight variation won't succeed. Here's the log for HelveticaNeue-UltraLight, retrieved by name: <UICTFont: 0x7bf91af0> font-family: "HelveticaNeue-UltraLight"; font-weight: normal; font-style: normal; font-size: 16.00pt.Nate Cook

5 Answers

12
votes

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"
        }
    ];
7
votes

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.

2
votes

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.

0
votes

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

0
votes

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