Skip to content

Commit 11c8bf3

Browse files
Adam Gleitmanfacebook-github-bot
authored andcommitted
Add Dynamic Type support for iOS (Paper and Fabric) (#35017)
Summary: This adds Dynamic Type support in iOS as described [here](react-native-community/discussions-and-proposals#519). `Text` elements have a new prop, `dynamicTypeRamp`, that allows users to specify which font ramp a particular `Text` element should take on as the OS's accessibility setting for text size. The different types line up with different values of `UIFontTextStyle`. If not specified, we default to the current behavior. ~~For the moment, this change is only for Paper. I tried applying a corresponding change to Fabric by adding an additional field to [`facebook::react::TextAttributes`](https://github.com/facebook/react-native/blob/main/ReactCommon/react/renderer/attributedstring/TextAttributes.h) and changing [`RCTEffectiveFontSizeMultiplierFromTextAttributes`](https://github.com/facebook/react-native/blob/afb124dcf0cdf0db525acc7cfd2cea2742c64068/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTAttributedTextUtils.mm#L79-L84) to use that new field, but in the process I discovered that this function doesn't seem to ever get called, hence [this bug](#34990 ## Changelog [iOS] [Added] - Dynamic Type support Pull Request resolved: #35017 Test Plan: Validated with a test page in RNTester. Screenshots follow: A) Default text size B) Largest non-accessibility text size C) Largest accessibility text size, split across two screenshots due to size | A | B | C | |-|-|-| | ![Simulator Screen Shot - iPad (9th generation) - 2022-10-18 at 16 17 08](https://user-images.githubusercontent.com/717674/196562746-c8bbf53d-3c70-4e55-8600-0cfed8aacf5d.png) | ![Simulator Screen Shot - iPad (9th generation) - 2022-10-18 at 16 17 55](https://user-images.githubusercontent.com/717674/196563051-68bb0e34-c573-47ed-8c19-58ae45a7ce2b.png) | ![Simulator Screen Shot - iPad (9th generation) - 2022-10-18 at 16 18 33](https://user-images.githubusercontent.com/717674/196563185-61ede5ee-e79e-4af5-84a7-8f1e230a25f8.png) | ||| ![Simulator Screen Shot - iPad (9th generation) - 2022-10-18 at 16 18 42](https://user-images.githubusercontent.com/717674/196563208-2242efa2-5f24-466d-80f5-eb57a7678a67.png) | Reviewed By: sammy-SC Differential Revision: D40779346 Pulled By: NickGerleman fbshipit-source-id: efc7a8e9810a93afc82c5def97af15a2e8453d90
1 parent 5fda72f commit 11c8bf3

File tree

18 files changed

+405
-1011
lines changed

18 files changed

+405
-1011
lines changed

BUCK

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ REACT_PUBLIC_HEADERS = {
267267
"React/RCTDevLoadingViewProtocol.h": RCTDEVSUPPORT_PATH + "RCTDevLoadingViewProtocol.h",
268268
"React/RCTDevLoadingViewSetEnabled.h": RCTDEVSUPPORT_PATH + "RCTDevLoadingViewSetEnabled.h",
269269
"React/RCTDisplayLink.h": RCTBASE_PATH + "RCTDisplayLink.h",
270+
"React/RCTDynamicTypeRamp.h": RCTLIB_PATH + "Text/Text/RCTDynamicTypeRamp.h",
270271
"React/RCTErrorCustomizer.h": RCTBASE_PATH + "RCTErrorCustomizer.h",
271272
"React/RCTErrorInfo.h": RCTBASE_PATH + "RCTErrorInfo.h",
272273
# NOTE: RCTEventDispatcher.h is exported from CoreModules:CoreModulesApple

Libraries/Text/BaseText/RCTBaseTextViewManager.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ - (RCTShadowView *)shadowView
3636
RCT_REMAP_SHADOW_PROPERTY(fontStyle, textAttributes.fontStyle, NSString)
3737
RCT_REMAP_SHADOW_PROPERTY(fontVariant, textAttributes.fontVariant, NSArray)
3838
RCT_REMAP_SHADOW_PROPERTY(allowFontScaling, textAttributes.allowFontScaling, BOOL)
39+
RCT_REMAP_SHADOW_PROPERTY(dynamicTypeRamp, textAttributes.dynamicTypeRamp, RCTDynamicTypeRamp)
3940
RCT_REMAP_SHADOW_PROPERTY(maxFontSizeMultiplier, textAttributes.maxFontSizeMultiplier, CGFloat)
4041
RCT_REMAP_SHADOW_PROPERTY(letterSpacing, textAttributes.letterSpacing, CGFloat)
4142
// Paragraph Styles

Libraries/Text/RCTTextAttributes.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#import <UIKit/UIKit.h>
99

10+
#import <React/RCTDynamicTypeRamp.h>
1011
#import <React/RCTTextDecorationLineType.h>
1112

1213
#import "RCTTextTransform.h"
@@ -36,6 +37,7 @@ extern NSString *const RCTTextAttributesTagAttributeName;
3637
@property (nonatomic, copy, nullable) NSString *fontStyle;
3738
@property (nonatomic, copy, nullable) NSArray<NSString *> *fontVariant;
3839
@property (nonatomic, assign) BOOL allowFontScaling;
40+
@property (nonatomic, assign) RCTDynamicTypeRamp dynamicTypeRamp;
3941
@property (nonatomic, assign) CGFloat letterSpacing;
4042
// Paragraph Styles
4143
@property (nonatomic, assign) CGFloat lineHeight;

Libraries/Text/RCTTextAttributes.m

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ - (void)applyTextAttributes:(RCTTextAttributes *)textAttributes
5959
_fontStyle = textAttributes->_fontStyle ?: _fontStyle;
6060
_fontVariant = textAttributes->_fontVariant ?: _fontVariant;
6161
_allowFontScaling = textAttributes->_allowFontScaling || _allowFontScaling; // *
62+
_dynamicTypeRamp = textAttributes->_dynamicTypeRamp != RCTDynamicTypeRampUndefined ? textAttributes->_dynamicTypeRamp
63+
: _dynamicTypeRamp;
6264
_letterSpacing = !isnan(textAttributes->_letterSpacing) ? textAttributes->_letterSpacing : _letterSpacing;
6365

6466
// Paragraph Styles
@@ -230,6 +232,12 @@ - (CGFloat)effectiveFontSizeMultiplier
230232

231233
if (fontScalingEnabled) {
232234
CGFloat fontSizeMultiplier = !isnan(_fontSizeMultiplier) ? _fontSizeMultiplier : 1.0;
235+
if (_dynamicTypeRamp != RCTDynamicTypeRampUndefined) {
236+
UIFontMetrics *fontMetrics = RCTUIFontMetricsForDynamicTypeRamp(_dynamicTypeRamp);
237+
// Using a specific font size reduces rounding errors from -scaledValueForValue:
238+
CGFloat requestedSize = isnan(_fontSize) ? RCTBaseSizeForDynamicTypeRamp(_dynamicTypeRamp) : _fontSize;
239+
fontSizeMultiplier = [fontMetrics scaledValueForValue:requestedSize] / requestedSize;
240+
}
233241
CGFloat maxFontSizeMultiplier = !isnan(_maxFontSizeMultiplier) ? _maxFontSizeMultiplier : 0.0;
234242
return maxFontSizeMultiplier >= 1.0 ? fminf(maxFontSizeMultiplier, fontSizeMultiplier) : fontSizeMultiplier;
235243
} else {
@@ -324,7 +332,7 @@ - (BOOL)isEqual:(RCTTextAttributes *)textAttributes
324332
RCTTextAttributesCompareFloats(_fontSizeMultiplier) && RCTTextAttributesCompareFloats(_maxFontSizeMultiplier) &&
325333
RCTTextAttributesCompareStrings(_fontWeight) && RCTTextAttributesCompareObjects(_fontStyle) &&
326334
RCTTextAttributesCompareObjects(_fontVariant) && RCTTextAttributesCompareOthers(_allowFontScaling) &&
327-
RCTTextAttributesCompareFloats(_letterSpacing) &&
335+
RCTTextAttributesCompareOthers(_dynamicTypeRamp) && RCTTextAttributesCompareFloats(_letterSpacing) &&
328336
// Paragraph Styles
329337
RCTTextAttributesCompareFloats(_lineHeight) && RCTTextAttributesCompareFloats(_alignment) &&
330338
RCTTextAttributesCompareOthers(_baseWritingDirection) && RCTTextAttributesCompareOthers(_lineBreakStrategy) &&

Libraries/Text/Text.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,23 @@ export interface TextPropsIOS {
2626
*/
2727
adjustsFontSizeToFit?: boolean | undefined;
2828

29+
/**
30+
* The Dynamic Text scale ramp to apply to this element on iOS.
31+
*/
32+
dynamicTypeRamp?:
33+
| 'caption2'
34+
| 'caption1'
35+
| 'footnote'
36+
| 'subheadline'
37+
| 'callout'
38+
| 'body'
39+
| 'headline'
40+
| 'title3'
41+
| 'title2'
42+
| 'title1'
43+
| 'largeTitle'
44+
| undefined;
45+
2946
/**
3047
* Specifies smallest possible scale a font can reach when adjustsFontSizeToFit is enabled. (values 0.01-1.0).
3148
*/
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <Foundation/Foundation.h>
9+
10+
#import <React/RCTConvert.h>
11+
12+
typedef NS_ENUM(NSInteger, RCTDynamicTypeRamp) {
13+
RCTDynamicTypeRampUndefined,
14+
RCTDynamicTypeRampCaption2,
15+
RCTDynamicTypeRampCaption1,
16+
RCTDynamicTypeRampFootnote,
17+
RCTDynamicTypeRampSubheadline,
18+
RCTDynamicTypeRampCallout,
19+
RCTDynamicTypeRampBody,
20+
RCTDynamicTypeRampHeadline,
21+
RCTDynamicTypeRampTitle3,
22+
RCTDynamicTypeRampTitle2,
23+
RCTDynamicTypeRampTitle1,
24+
RCTDynamicTypeRampLargeTitle
25+
};
26+
27+
@interface RCTConvert (DynamicTypeRamp)
28+
29+
+ (RCTDynamicTypeRamp)RCTDynamicTypeRamp:(nullable id)json;
30+
31+
@end
32+
33+
/// Generates a `UIFontMetrics` instance representing a particular Dynamic Type ramp.
34+
UIFontMetrics *_Nonnull RCTUIFontMetricsForDynamicTypeRamp(RCTDynamicTypeRamp dynamicTypeRamp);
35+
/// The "reference" size for a particular font scale ramp, equal to a text element's size under default text size
36+
/// settings.
37+
CGFloat RCTBaseSizeForDynamicTypeRamp(RCTDynamicTypeRamp dynamicTypeRamp);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <React/RCTDynamicTypeRamp.h>
9+
10+
@implementation RCTConvert (DynamicTypeRamp)
11+
12+
RCT_ENUM_CONVERTER(
13+
RCTDynamicTypeRamp,
14+
(@{
15+
@"caption2" : @(RCTDynamicTypeRampCaption2),
16+
@"caption1" : @(RCTDynamicTypeRampCaption1),
17+
@"footnote" : @(RCTDynamicTypeRampFootnote),
18+
@"subheadline" : @(RCTDynamicTypeRampSubheadline),
19+
@"callout" : @(RCTDynamicTypeRampCallout),
20+
@"body" : @(RCTDynamicTypeRampBody),
21+
@"headline" : @(RCTDynamicTypeRampHeadline),
22+
@"title3" : @(RCTDynamicTypeRampTitle3),
23+
@"title2" : @(RCTDynamicTypeRampTitle2),
24+
@"title1" : @(RCTDynamicTypeRampTitle1),
25+
@"largeTitle" : @(RCTDynamicTypeRampLargeTitle),
26+
}),
27+
RCTDynamicTypeRampUndefined,
28+
integerValue)
29+
30+
@end
31+
32+
UIFontMetrics *RCTUIFontMetricsForDynamicTypeRamp(RCTDynamicTypeRamp dynamicTypeRamp)
33+
{
34+
static NSDictionary<NSNumber *, UIFontTextStyle> *mapping;
35+
static dispatch_once_t onceToken;
36+
dispatch_once(&onceToken, ^{
37+
mapping = @{
38+
@(RCTDynamicTypeRampCaption2) : UIFontTextStyleCaption2,
39+
@(RCTDynamicTypeRampCaption1) : UIFontTextStyleCaption1,
40+
@(RCTDynamicTypeRampFootnote) : UIFontTextStyleFootnote,
41+
@(RCTDynamicTypeRampSubheadline) : UIFontTextStyleSubheadline,
42+
@(RCTDynamicTypeRampCallout) : UIFontTextStyleCallout,
43+
@(RCTDynamicTypeRampBody) : UIFontTextStyleBody,
44+
@(RCTDynamicTypeRampHeadline) : UIFontTextStyleHeadline,
45+
@(RCTDynamicTypeRampTitle3) : UIFontTextStyleTitle3,
46+
@(RCTDynamicTypeRampTitle2) : UIFontTextStyleTitle2,
47+
@(RCTDynamicTypeRampTitle1) : UIFontTextStyleTitle1,
48+
@(RCTDynamicTypeRampLargeTitle) : UIFontTextStyleLargeTitle,
49+
};
50+
});
51+
52+
id textStyle =
53+
mapping[@(dynamicTypeRamp)] ?: UIFontTextStyleBody; // Default to body if we don't recognize the specified ramp
54+
return [UIFontMetrics metricsForTextStyle:textStyle];
55+
}
56+
57+
CGFloat RCTBaseSizeForDynamicTypeRamp(RCTDynamicTypeRamp dynamicTypeRamp)
58+
{
59+
static NSDictionary<NSNumber *, NSNumber *> *mapping;
60+
static dispatch_once_t onceToken;
61+
dispatch_once(&onceToken, ^{
62+
// Values taken from
63+
// https://developer.apple.com/design/human-interface-guidelines/foundations/typography/#specifications
64+
mapping = @{
65+
@(RCTDynamicTypeRampCaption2) : @11,
66+
@(RCTDynamicTypeRampCaption1) : @12,
67+
@(RCTDynamicTypeRampFootnote) : @13,
68+
@(RCTDynamicTypeRampSubheadline) : @15,
69+
@(RCTDynamicTypeRampCallout) : @16,
70+
@(RCTDynamicTypeRampBody) : @17,
71+
@(RCTDynamicTypeRampHeadline) : @17,
72+
@(RCTDynamicTypeRampTitle3) : @20,
73+
@(RCTDynamicTypeRampTitle2) : @22,
74+
@(RCTDynamicTypeRampTitle1) : @28,
75+
@(RCTDynamicTypeRampLargeTitle) : @34,
76+
};
77+
});
78+
79+
NSNumber *baseSize =
80+
mapping[@(dynamicTypeRamp)] ?: @17; // Default to body size if we don't recognize the specified ramp
81+
return CGFLOAT_IS_DOUBLE ? [baseSize doubleValue] : [baseSize floatValue];
82+
}

Libraries/Text/TextNativeComponent.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const textViewConfig = {
3434
numberOfLines: true,
3535
ellipsizeMode: true,
3636
allowFontScaling: true,
37+
dynamicTypeRamp: true,
3738
maxFontSizeMultiplier: true,
3839
disabled: true,
3940
selectable: true,

Libraries/Text/TextProps.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,23 @@ export type TextProps = $ReadOnly<{|
228228
*/
229229
adjustsFontSizeToFit?: ?boolean,
230230

231+
/**
232+
* The Dynamic Text scale ramp to apply to this element on iOS.
233+
*/
234+
dynamicTypeRamp?: ?(
235+
| 'caption2'
236+
| 'caption1'
237+
| 'footnote'
238+
| 'subheadline'
239+
| 'callout'
240+
| 'body'
241+
| 'headline'
242+
| 'title3'
243+
| 'title2'
244+
| 'title1'
245+
| 'largeTitle'
246+
),
247+
231248
/**
232249
* Smallest possible scale a font can reach.
233250
*

ReactCommon/react/renderer/attributedstring/TextAttributes.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ void TextAttributes::apply(TextAttributes textAttributes) {
4646
allowFontScaling = textAttributes.allowFontScaling.has_value()
4747
? textAttributes.allowFontScaling
4848
: allowFontScaling;
49+
dynamicTypeRamp = textAttributes.dynamicTypeRamp.has_value()
50+
? textAttributes.dynamicTypeRamp
51+
: dynamicTypeRamp;
4952
letterSpacing = !std::isnan(textAttributes.letterSpacing)
5053
? textAttributes.letterSpacing
5154
: letterSpacing;
@@ -111,6 +114,7 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const {
111114
fontStyle,
112115
fontVariant,
113116
allowFontScaling,
117+
dynamicTypeRamp,
114118
alignment,
115119
baseWritingDirection,
116120
lineBreakStrategy,
@@ -131,6 +135,7 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const {
131135
rhs.fontStyle,
132136
rhs.fontVariant,
133137
rhs.allowFontScaling,
138+
rhs.dynamicTypeRamp,
134139
rhs.alignment,
135140
rhs.baseWritingDirection,
136141
rhs.lineBreakStrategy,
@@ -186,6 +191,7 @@ SharedDebugStringConvertibleList TextAttributes::getDebugProps() const {
186191
debugStringConvertibleItem("fontStyle", fontStyle),
187192
debugStringConvertibleItem("fontVariant", fontVariant),
188193
debugStringConvertibleItem("allowFontScaling", allowFontScaling),
194+
debugStringConvertibleItem("dynamicTypeRamp", dynamicTypeRamp),
189195
debugStringConvertibleItem("letterSpacing", letterSpacing),
190196

191197
// Paragraph Styles

0 commit comments

Comments
 (0)