Skip to content

Instantly share code, notes, and snippets.

@jupdike
Created March 9, 2021 01:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jupdike/50191372199380c7a92145d8e9187105 to your computer and use it in GitHub Desktop.
Save jupdike/50191372199380c7a92145d8e9187105 to your computer and use it in GitHub Desktop.
Hacks to React Native iOS font selecting code (updateFont method) to allow picking width, optical size for complicated font families
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* 2021-03-08 hacks by Jared Updike, to updateFont method.
* Allow picking width, optical size for complicated font families. For example I have a design using Merriweather
* with these font files:
Merriweather-12ptBold.ttf
Merriweather-12ptBoldItalic.ttf
Merriweather-12ptItalic.ttf
Merriweather-12ptRegular.ttf
Merriweather-144ptSemiCondensedBlackItalic.ttf
Merriweather-144ptSemiExpandedBlackItalic.ttf
Merriweather-60ptSemiExpandedRegular.ttf
Merriweather-6ptSemiExpandedBlackItalic.ttf
Merriweather-96ptSemiExpandedLight.ttf
* and I need to be able to pick the exact style in my StyleSheet in JavaScript, by setting the font family to the
* name of the font file without the extension. Requires the font files to be named with the a prefix of
* family name plus hyphen. The system knows that the fonts are all Merriweather font family, but without this hack,
* React Native has no way to pick the exact style I want (no width like expanded, condensed, no 12pt or 60pt or whatever).
*/
#import "RCTFont.h"
#import "RCTAssert.h"
#import "RCTLog.h"
#import <CoreText/CoreText.h>
#import <mutex>
typedef CGFloat RCTFontWeight;
static RCTFontWeight weightOfFont(UIFont *font)
{
static NSArray *fontNames;
static NSArray *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.
fontNames = @[
@"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)
];
});
for (NSInteger i = 0; i < 0 || i < (unsigned)fontNames.count; i++) {
if ([font.fontName.lowercaseString hasSuffix:fontNames[i]]) {
return (RCTFontWeight)[fontWeights[i] doubleValue];
}
}
NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute];
return (RCTFontWeight)[traits[UIFontWeightTrait] doubleValue];
}
static BOOL isItalicFont(UIFont *font)
{
NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute];
UIFontDescriptorSymbolicTraits symbolicTraits = [traits[UIFontSymbolicTrait] unsignedIntValue];
return (symbolicTraits & UIFontDescriptorTraitItalic) != 0;
}
static BOOL isCondensedFont(UIFont *font)
{
NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute];
UIFontDescriptorSymbolicTraits symbolicTraits = [traits[UIFontSymbolicTrait] unsignedIntValue];
return (symbolicTraits & UIFontDescriptorTraitCondensed) != 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 *fontCache;
static std::mutex *fontCacheMutex = new std::mutex;
NSString *cacheKey = [NSString stringWithFormat:@"%.1f/%.2f", size, weight];
UIFont *font;
{
std::lock_guard<std::mutex> lock(*fontCacheMutex);
if (!fontCache) {
fontCache = [NSCache new];
}
font = [fontCache objectForKey:cacheKey];
}
if (!font) {
if (defaultFontHandler) {
NSString *fontWeightDescription = FontWeightDescriptionFromUIFontWeight(weight);
font = defaultFontHandler(size, fontWeightDescription);
} else {
font = [UIFont systemFontOfSize:size weight:weight];
}
{
std::lock_guard<std::mutex> lock(*fontCacheMutex);
[fontCache setObject:font forKey:cacheKey];
}
}
return font;
}
@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];
}
}
}
NSString* justFamily = familyName;
if ([familyName containsString:@"-"]) {
NSArray* array = [familyName componentsSeparatedByString:@"-"];
justFamily = array[0];
}
//NSLog(@"justFamily: %@", justFamily);
// Gracefully handle being given a font name rather than font family, for
// example: "Helvetica Light Oblique" rather than just "Helvetica".
if (!didFindFont && [UIFont fontNamesForFamilyName:justFamily].count == 0) {
NSLog(@"111 did not find");
font = [UIFont fontWithName:familyName size:fontSize];
if (font) {
NSLog(@"222 did find %@", familyName);
// 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 {
NSLog(@"333 did NOT find");
// 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];
}
}
}
if (!font) {
// Get the closest font that matches the given weight for the fontFamily
CGFloat closestWeight = INFINITY;
for (NSString *name in [UIFont fontNamesForFamilyName:justFamily]) {
UIFont *match = [UIFont fontWithName:name size:fontSize];
//NSLog(@"justFamily: %@, familyName: %@, name: %@", justFamily, familyName, name);
// if (isItalic == isItalicFont(match) && isCondensed == isCondensedFont(match)) {
// CGFloat testWeight = weightOfFont(match);
// if (ABS(testWeight - fontWeight) < ABS(closestWeight - fontWeight)) {
// font = match;
// closestWeight = testWeight;
// }
// }
// TODOx
// if something
if ([name caseInsensitiveCompare:familyName] == NSOrderedSame) {
font = match;
//NSLog(@"found a match: %@", familyName);
break;
}
}
}
// 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) {
NSArray *names = [UIFont fontNamesForFamilyName:familyName];
if (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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment