Files
react-native/React/Views/RCTFont.mm
T
Danilo Bürger 89efa1a0c1 Only find closest font if system font was not found (#32482)
Summary:
Before https://github.com/facebook/react-native/commit/f951da912dd8b4dc2b0d674f8f37f9d982a03c48 finding a system font used to return early. In order to allow variants, the referenced patch removed the early return so that variants could be applied later. However, there is no need to find the closest font as we already selected the proper system font. This also fixes a bug with setting a custom font handler via RCTSetDefaultFontHandler whos return could get overwritten by the closest font search.

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->

[iOS] [Fixed] - Respect RCTSetDefaultFontHandler chosen font

Pull Request resolved: https://github.com/facebook/react-native/pull/32482

Reviewed By: ShikaSD

Differential Revision: D33844138

Pulled By: cortinico

fbshipit-source-id: 05c01fc358cd19f8be342218cdba944b303073ed
2022-01-28 07:38:39 -08:00

412 lines
13 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),
},
};
});
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
RCTLogError(@"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