Files
react-native/React/Views/RCTFont.mm
T
Moti Zilberman d6e9891577 Reduce log level of "unrecognized font family" from Error to Info
Summary:
Changelog:
[iOS][Fixed] - Unrecognized fontFamily values no longer trigger a redbox

## Context

We are starting to roll out a new consistent approach to style errors in React Native, where malformed or semantically invalid style values will never be treated as runtime errors. Instead, bad style values will consistently fall back to known defaults; the incorrect visual rendering will serve as the main runtime diagnostic for developers. We will advise developers to rely on static types for additional diagnostics.

This work will take place over multiple commits and possibly multiple releases of React Native.

## This diff

Here we fix this issue as it applies to the `fontFamily` style prop. The legacy (Paper) renderer on iOS is the only concrete implementation that had to change. Fabric and Android already implement the correct behaviour.

h/t EvanBacon for the report: https://twitter.com/Baconbrix/status/1623039650775371792

Reviewed By: huntie

Differential Revision: D43159284

fbshipit-source-id: 6afeef3abc5781e18671708f642073d25f2347e9
2023-02-09 10:57:50 -08:00

492 lines
17 KiB
Plaintext

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTFont.h"
#import "RCTAssert.h"
#import "RCTLog.h"
#import <CoreText/CoreText.h>
typedef CGFloat RCTFontWeight;
static RCTFontWeight weightOfFont(UIFont *font)
{
static NSArray<NSString *> *weightSuffixes;
static NSArray<NSNumber *> *fontWeights;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// We use two arrays instead of one map because
// the order is important for suffix matching.
weightSuffixes = @[
@"normal",
@"ultralight",
@"thin",
@"light",
@"regular",
@"medium",
@"semibold",
@"demibold",
@"extrabold",
@"ultrabold",
@"bold",
@"heavy",
@"black"
];
fontWeights = @[
@(UIFontWeightRegular),
@(UIFontWeightUltraLight),
@(UIFontWeightThin),
@(UIFontWeightLight),
@(UIFontWeightRegular),
@(UIFontWeightMedium),
@(UIFontWeightSemibold),
@(UIFontWeightSemibold),
@(UIFontWeightHeavy),
@(UIFontWeightHeavy),
@(UIFontWeightBold),
@(UIFontWeightHeavy),
@(UIFontWeightBlack)
];
});
NSString *fontName = font.fontName;
NSInteger i = 0;
for (NSString *suffix in weightSuffixes) {
// CFStringFind is much faster than any variant of rangeOfString: because it does not use a locale.
auto options = kCFCompareCaseInsensitive | kCFCompareAnchored | kCFCompareBackwards;
if (CFStringFind((CFStringRef)fontName, (CFStringRef)suffix, options).location != kCFNotFound) {
return (RCTFontWeight)fontWeights[i].doubleValue;
}
i++;
}
auto traits = (__bridge_transfer NSDictionary *)CTFontCopyTraits((CTFontRef)font);
return (RCTFontWeight)[traits[UIFontWeightTrait] doubleValue];
}
static BOOL isItalicFont(UIFont *font)
{
return (CTFontGetSymbolicTraits((CTFontRef)font) & kCTFontTraitItalic) != 0;
}
static BOOL isCondensedFont(UIFont *font)
{
return (CTFontGetSymbolicTraits((CTFontRef)font) & kCTFontTraitCondensed) != 0;
}
static RCTFontHandler defaultFontHandler;
void RCTSetDefaultFontHandler(RCTFontHandler handler)
{
defaultFontHandler = handler;
}
BOOL RCTHasFontHandlerSet()
{
return defaultFontHandler != nil;
}
// We pass a string description of the font weight to the defaultFontHandler because UIFontWeight
// is not defined pre-iOS 8.2.
// Furthermore, UIFontWeight's are lossy floats, so we must use an inexact compare to figure out
// which one we actually have.
static inline BOOL CompareFontWeights(UIFontWeight firstWeight, UIFontWeight secondWeight)
{
#if CGFLOAT_IS_DOUBLE
return fabs(firstWeight - secondWeight) < 0.01;
#else
return fabsf(firstWeight - secondWeight) < 0.01;
#endif
}
static NSString *FontWeightDescriptionFromUIFontWeight(UIFontWeight fontWeight)
{
if (CompareFontWeights(fontWeight, UIFontWeightUltraLight)) {
return @"ultralight";
} else if (CompareFontWeights(fontWeight, UIFontWeightThin)) {
return @"thin";
} else if (CompareFontWeights(fontWeight, UIFontWeightLight)) {
return @"light";
} else if (CompareFontWeights(fontWeight, UIFontWeightRegular)) {
return @"regular";
} else if (CompareFontWeights(fontWeight, UIFontWeightMedium)) {
return @"medium";
} else if (CompareFontWeights(fontWeight, UIFontWeightSemibold)) {
return @"semibold";
} else if (CompareFontWeights(fontWeight, UIFontWeightBold)) {
return @"bold";
} else if (CompareFontWeights(fontWeight, UIFontWeightHeavy)) {
return @"heavy";
} else if (CompareFontWeights(fontWeight, UIFontWeightBlack)) {
return @"black";
}
RCTAssert(NO, @"Unknown UIFontWeight passed in: %f", fontWeight);
return @"regular";
}
static UIFont *cachedSystemFont(CGFloat size, RCTFontWeight weight)
{
static NSCache<NSValue *, UIFont *> *fontCache = [NSCache new];
struct __attribute__((__packed__)) CacheKey {
CGFloat size;
RCTFontWeight weight;
};
CacheKey key{size, weight};
NSValue *cacheKey = [[NSValue alloc] initWithBytes:&key objCType:@encode(CacheKey)];
UIFont *font = [fontCache objectForKey:cacheKey];
if (!font) {
if (defaultFontHandler) {
NSString *fontWeightDescription = FontWeightDescriptionFromUIFontWeight(weight);
font = defaultFontHandler(size, fontWeightDescription);
} else {
font = [UIFont systemFontOfSize:size weight:weight];
}
[fontCache setObject:font forKey:cacheKey];
}
return font;
}
// Caching wrapper around expensive +[UIFont fontNamesForFamilyName:]
static NSArray<NSString *> *fontNamesForFamilyName(NSString *familyName)
{
static NSCache<NSString *, NSArray<NSString *> *> *cache;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
cache = [NSCache new];
[NSNotificationCenter.defaultCenter
addObserverForName:(NSNotificationName)kCTFontManagerRegisteredFontsChangedNotification
object:nil
queue:nil
usingBlock:^(NSNotification *) {
[cache removeAllObjects];
}];
});
auto names = [cache objectForKey:familyName];
if (!names) {
names = [UIFont fontNamesForFamilyName:familyName] ?: [NSArray new];
[cache setObject:names forKey:familyName];
}
return names;
}
@implementation RCTConvert (RCTFont)
+ (UIFont *)UIFont:(id)json
{
json = [self NSDictionary:json];
return [RCTFont updateFont:nil
withFamily:[RCTConvert NSString:json[@"fontFamily"]]
size:[RCTConvert NSNumber:json[@"fontSize"]]
weight:[RCTConvert NSString:json[@"fontWeight"]]
style:[RCTConvert NSString:json[@"fontStyle"]]
variant:[RCTConvert NSStringArray:json[@"fontVariant"]]
scaleMultiplier:1];
}
RCT_ENUM_CONVERTER(
RCTFontWeight,
(@{
@"normal" : @(UIFontWeightRegular),
@"bold" : @(UIFontWeightBold),
@"100" : @(UIFontWeightUltraLight),
@"200" : @(UIFontWeightThin),
@"300" : @(UIFontWeightLight),
@"400" : @(UIFontWeightRegular),
@"500" : @(UIFontWeightMedium),
@"600" : @(UIFontWeightSemibold),
@"700" : @(UIFontWeightBold),
@"800" : @(UIFontWeightHeavy),
@"900" : @(UIFontWeightBlack),
}),
UIFontWeightRegular,
doubleValue)
typedef BOOL RCTFontStyle;
RCT_ENUM_CONVERTER(
RCTFontStyle,
(@{
@"normal" : @NO,
@"italic" : @YES,
@"oblique" : @YES,
}),
NO,
boolValue)
typedef NSDictionary RCTFontVariantDescriptor;
+ (RCTFontVariantDescriptor *)RCTFontVariantDescriptor:(id)json
{
static NSDictionary *mapping;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mapping = @{
@"small-caps" : @{
UIFontFeatureTypeIdentifierKey : @(kLowerCaseType),
UIFontFeatureSelectorIdentifierKey : @(kLowerCaseSmallCapsSelector),
},
@"oldstyle-nums" : @{
UIFontFeatureTypeIdentifierKey : @(kNumberCaseType),
UIFontFeatureSelectorIdentifierKey : @(kLowerCaseNumbersSelector),
},
@"lining-nums" : @{
UIFontFeatureTypeIdentifierKey : @(kNumberCaseType),
UIFontFeatureSelectorIdentifierKey : @(kUpperCaseNumbersSelector),
},
@"tabular-nums" : @{
UIFontFeatureTypeIdentifierKey : @(kNumberSpacingType),
UIFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector),
},
@"proportional-nums" : @{
UIFontFeatureTypeIdentifierKey : @(kNumberSpacingType),
UIFontFeatureSelectorIdentifierKey : @(kProportionalNumbersSelector),
},
@"stylistic-one" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltOneOnSelector),
},
@"stylistic-two" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltTwoOnSelector),
},
@"stylistic-three" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltThreeOnSelector),
},
@"stylistic-four" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltFourOnSelector),
},
@"stylistic-five" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltFiveOnSelector),
},
@"stylistic-six" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltSixOnSelector),
},
@"stylistic-seven" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltSevenOnSelector),
},
@"stylistic-eight" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltEightOnSelector),
},
@"stylistic-nine" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltNineOnSelector),
},
@"stylistic-ten" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltTenOnSelector),
},
@"stylistic-eleven" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltElevenOnSelector),
},
@"stylistic-twelve" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltTwelveOnSelector),
},
@"stylistic-thirteen" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltThirteenOnSelector),
},
@"stylistic-fourteen" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltFourteenOnSelector),
},
@"stylistic-fifteen" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltFifteenOnSelector),
},
@"stylistic-sixteen" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltSixteenOnSelector),
},
@"stylistic-seventeen" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltSeventeenOnSelector),
},
@"stylistic-eighteen" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltEighteenOnSelector),
},
@"stylistic-nineteen" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltNineteenOnSelector),
},
@"stylistic-twenty" : @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltTwentyOnSelector),
}
};
});
RCTFontVariantDescriptor *value = mapping[json];
if (RCT_DEBUG && !value && [json description].length > 0) {
RCTLogError(
@"Invalid RCTFontVariantDescriptor '%@'. should be one of: %@",
json,
[[mapping allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]);
}
return value;
}
RCT_ARRAY_CONVERTER(RCTFontVariantDescriptor)
@end
@implementation RCTFont
+ (UIFont *)updateFont:(UIFont *)font
withFamily:(NSString *)family
size:(NSNumber *)size
weight:(NSString *)weight
style:(NSString *)style
variant:(NSArray<RCTFontVariantDescriptor *> *)variant
scaleMultiplier:(CGFloat)scaleMultiplier
{
// Defaults
static NSString *defaultFontFamily;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
defaultFontFamily = [UIFont systemFontOfSize:14].familyName;
});
const RCTFontWeight defaultFontWeight = UIFontWeightRegular;
const CGFloat defaultFontSize = 14;
// Initialize properties to defaults
CGFloat fontSize = defaultFontSize;
RCTFontWeight fontWeight = defaultFontWeight;
NSString *familyName = defaultFontFamily;
BOOL isItalic = NO;
BOOL isCondensed = NO;
if (font) {
familyName = font.familyName ?: defaultFontFamily;
fontSize = font.pointSize ?: defaultFontSize;
fontWeight = weightOfFont(font);
isItalic = isItalicFont(font);
isCondensed = isCondensedFont(font);
}
// Get font attributes
fontSize = [RCTConvert CGFloat:size] ?: fontSize;
if (scaleMultiplier > 0.0 && scaleMultiplier != 1.0) {
fontSize = round(fontSize * scaleMultiplier);
}
familyName = [RCTConvert NSString:family] ?: familyName;
isItalic = style ? [RCTConvert RCTFontStyle:style] : isItalic;
fontWeight = weight ? [RCTConvert RCTFontWeight:weight] : fontWeight;
BOOL didFindFont = NO;
// Handle system font as special case. This ensures that we preserve
// the specific metrics of the standard system font as closely as possible.
if ([familyName isEqual:defaultFontFamily] || [familyName isEqualToString:@"System"]) {
font = cachedSystemFont(fontSize, fontWeight);
if (font) {
didFindFont = YES;
if (isItalic || isCondensed) {
UIFontDescriptor *fontDescriptor = [font fontDescriptor];
UIFontDescriptorSymbolicTraits symbolicTraits = fontDescriptor.symbolicTraits;
if (isItalic) {
symbolicTraits |= UIFontDescriptorTraitItalic;
}
if (isCondensed) {
symbolicTraits |= UIFontDescriptorTraitCondensed;
}
fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits];
font = [UIFont fontWithDescriptor:fontDescriptor size:fontSize];
}
}
}
// Gracefully handle being given a font name rather than font family, for
// example: "Helvetica Light Oblique" rather than just "Helvetica".
if (!didFindFont && fontNamesForFamilyName(familyName).count == 0) {
font = [UIFont fontWithName:familyName size:fontSize];
if (font) {
// It's actually a font name, not a font family name,
// but we'll do what was meant, not what was said.
familyName = font.familyName;
fontWeight = weight ? fontWeight : weightOfFont(font);
isItalic = style ? isItalic : isItalicFont(font);
isCondensed = isCondensedFont(font);
} else {
// Not a valid font or family
RCTLogInfo(@"Unrecognized font family '%@'", familyName);
if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) {
font = [UIFont systemFontOfSize:fontSize weight:fontWeight];
} else if (fontWeight > UIFontWeightRegular) {
font = [UIFont boldSystemFontOfSize:fontSize];
} else {
font = [UIFont systemFontOfSize:fontSize];
}
}
}
NSArray<NSString *> *names = fontNamesForFamilyName(familyName);
if (!didFindFont) {
// Get the closest font that matches the given weight for the fontFamily
CGFloat closestWeight = INFINITY;
for (NSString *name in names) {
UIFont *match = [UIFont fontWithName:name size:fontSize];
if (isItalic == isItalicFont(match) && isCondensed == isCondensedFont(match)) {
CGFloat testWeight = weightOfFont(match);
if (ABS(testWeight - fontWeight) < ABS(closestWeight - fontWeight)) {
font = match;
closestWeight = testWeight;
}
}
}
}
// If we still don't have a match at least return the first font in the fontFamily
// This is to support built-in font Zapfino and other custom single font families like Impact
if (!font && names.count > 0) {
font = [UIFont fontWithName:names[0] size:fontSize];
}
// Apply font variants to font object
if (variant) {
NSArray *fontFeatures = [RCTConvert RCTFontVariantDescriptorArray:variant];
UIFontDescriptor *fontDescriptor = [font.fontDescriptor
fontDescriptorByAddingAttributes:@{UIFontDescriptorFeatureSettingsAttribute : fontFeatures}];
font = [UIFont fontWithDescriptor:fontDescriptor size:fontSize];
}
return font;
}
+ (UIFont *)updateFont:(UIFont *)font withFamily:(NSString *)family
{
return [self updateFont:font withFamily:family size:nil weight:nil style:nil variant:nil scaleMultiplier:1];
}
+ (UIFont *)updateFont:(UIFont *)font withSize:(NSNumber *)size
{
return [self updateFont:font withFamily:nil size:size weight:nil style:nil variant:nil scaleMultiplier:1];
}
+ (UIFont *)updateFont:(UIFont *)font withWeight:(NSString *)weight
{
return [self updateFont:font withFamily:nil size:nil weight:weight style:nil variant:nil scaleMultiplier:1];
}
+ (UIFont *)updateFont:(UIFont *)font withStyle:(NSString *)style
{
return [self updateFont:font withFamily:nil size:nil weight:nil style:style variant:nil scaleMultiplier:1];
}
@end