New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS: Support allowFontScaling on TextInput #14030

Closed
wants to merge 5 commits into
base: master
from

Conversation

Projects
5 participants
@rigdern
Contributor

rigdern commented May 18, 2017

Motivation (required)

Currently, only Text supports the allowFontScaling prop. This commit adds support for it on TextInput.

Notes

As part of this change, the TextInput setters for font attributes (e.g. size, weight) had to be refactored. The problem with them is that they use RCTFont's helpers which create a new font based on an existing font. These helpers lose information. In particular, they lose the scaleMultiplier.

For example, suppose the font size is 12 and the device's font multiplier is set to 1.5. So we'd create a font with size 12 and scaleMultiplier 1.5 which is an effective size of 18 (which is the only thing stored in the font). Next, suppose the device's font multiplier changes to 1. So we'd use an RCTFont helper to create a new font based on the existing font but with a scaleMultiplier of 1. However, the font didn't store the font size (12) and scaleMultiplier (1.5) separately. It just knows the (effective) font size of 18. So RCTFont thinks the new font has a font size of 18 and a scaleMultiplier of 1 so its effective font size is 18. This is incorrect and it should have been 12.

To fix this, the font attributes are now all stored individually. Anytime one of them changes, updateFont is called which recreates the font from scratch. This happens to fix some bugs around fontStyle and fontWeight which were reported several times before: #13730, #12738, #2140, #8533.

Test Plan (required)

Created a test app where I verified that allowFontScaling works properly for TextInputs for all values (undefined, true, false) for a variety of TextInputs:

  • Singleline TextInput
  • Singleline TextInput's placeholder
  • Multiline TextInput
  • Multiline TextInput's placeholder
  • Multiline TextInput using children instead of value

Also, verified switching fontSize, fontWeight, fontStyle and fontFamily through a bunch of combinations works properly.

Lastly, my team has been using this change in our app.

Adam Comella
Microsoft Corp.

@jacobp100

This comment has been minimized.

Contributor

jacobp100 commented May 19, 2017

It would be good if we could have support for font variants. You can get a font variant from RCTFontVariantDescriptor!

@shergin shergin self-requested a review May 26, 2017

@shergin shergin added the 🔷iOS label May 26, 2017

@shergin shergin added this to Uncategorized in Better <TextInput> Jun 1, 2017

@shergin

This comment has been minimized.

Contributor

shergin commented Jun 23, 2017

(t19597801)

@shergin

This comment has been minimized.

Contributor

shergin commented Jun 23, 2017

@rigdern That's amazing! Thank you! Right now I am in the middle of huge refactoring of <TextInput>, so I will be able to merge it only after the refactoring is done. I also plan to decouple all font-related logic into separate class, so we can keep managers and view classes with RN specific data-flow logic as simple as possible.

@rigdern

This comment has been minimized.

Contributor

rigdern commented Aug 28, 2017

@shergin has your TextInput refactoring been merged to master?

@shergin

This comment has been minimized.

Contributor

shergin commented Aug 29, 2017

Yes, kind of. I merged all of what I had iOS TextInput related.

iOS: Support allowFontScaling on TextInput
Currently, only Text supports the allowFontScaling prop. This commit adds support for it on TextInput.

As part of this change, the TextInput setters for font attributes (e.g. size, weight) had to be refactored. The problem with them is that they use RCTFont's helpers which create a new font based on an existing font. These helpers lose information. In particular, they lose the scaleMultiplier. For example, suppose the font size is 12 and the device's font multiplier is set to 1.5. So we'd create a font with size 12 and scaleMultiplier 1.5 which is an effective size of 18 (which is the only thing stored in the font). Next, suppose the device's font multiplier changes to 1. So we'd use an RCTFont helper to create a new font based on the existing font but with a scaleMultiplier of 1. However, the font didn't store the font size (12) and scaleMultiplier (1.5) separately. It just knows the (effective) font size of 18. So RCTFont thinks the new font has a font size of 18 and a scaleMultiplier of 1 so its effective font size is 18. This is incorrect and it should have been 12.

To fix this, the font attributes are now all stored individually. Anytime one of them changes, updateFont is called which recreates the font from scratch. This happens to fix some bugs around fontStyle and fontWeight which were reported several times before: #13730, #12738, #2140, #8533.
@pull-bot

This comment has been minimized.

pull-bot commented Aug 31, 2017

Attention: @shergin

Generated by 🚫 dangerJS

@shergin

shergin requested changes Sep 6, 2017 edited

I really like the direction of this approach.
But here is how we can do it even better:

  • Make a font RCTTextInput's prop.
  • Create a new class, something like RCTFontAttributes (We already have RCTFontDescriptor, we have to also be sure that we are not reinventing this.)
  • RCTFontAttributes should have all font specific props; getter which returns UIFont instance; a delegate with one method fontAttributesDidChangeWithFont:
  • Have NSNotificationCenter related stuff inside that class;
  • Add a prop to RCTTextInput fontAttributes;
  • Make a RCTTextInput a delegate and implement that method as self.font = font;;
  • Redirect all font specific props in ViewManager to view.fontAttributes.xxxx.

And then later, finally, we can reuse it inside RCTText and co.

@rigdern

This comment has been minimized.

Contributor

rigdern commented Sep 7, 2017

@shergin I didn't have time to test it yet but is my latest commit along the lines that you were thinking?

A tricky detail was that RCTTextInput can't set font in its constructor because that would cause RCTTextView to attempt to set font on _backedTextInput before _backedTextInput was created. Instead, RCTTextInput subclasses manually set font in their constructors (the previous implementation had a similar problem but I didn't notice).

@shergin

Yes, yes, yes.

@@ -27,8 +28,10 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
RCTAssertParam(bridge);
if (self = [super initWithFrame:CGRectZero]) {
_bridge = bridge;
_bridge = bridge;

This comment has been minimized.

@shergin

shergin Sep 7, 2017

Contributor

Code style.

_eventDispatcher = bridge.eventDispatcher;
_fontAttributes = [[RCTFontAttributes alloc ] initWithAccessibilityManager:bridge.accessibilityManager];

This comment has been minimized.

@shergin

shergin Sep 7, 2017

Contributor

Code style.

@@ -70,6 +73,18 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
return _backedTextInput;
}
- (void)fontAttributesDidChangeWithFont:(RCTFont *)font
{
[super fontAttributesDidChangeWithFont:fonte];

This comment has been minimized.

@shergin

shergin Sep 7, 2017

Contributor

fonte? French? 😄

@@ -35,6 +35,7 @@ - (UIView *)view
#pragma mark - Unified <TextInput> properties
RCT_REMAP_VIEW_PROPERTY(allowFontScaling, fontAttributes.allowFontScaling, BOOL)

This comment has been minimized.

@shergin

shergin Sep 7, 2017

Contributor

We also have to remap other font-related props like this one.

This comment has been minimized.

@rigdern

rigdern Sep 7, 2017

Contributor

What props are you talking about? What should they be remapped to? It looks like you are highlighting allowFontScaling which is already getting remapped to fontAttributes.

@@ -66,19 +67,19 @@ - (UIView *)view
RCT_CUSTOM_VIEW_PROPERTY(fontSize, NSNumber, RCTTextView)
{
view.font = [RCTFont updateFont:view.font withSize:json ?: @(defaultView.font.pointSize)];
view.fontAttributes.fontSize = json ?: @(defaultView.font.pointSize);

This comment has been minimized.

@shergin

shergin Sep 7, 2017

Contributor

... and get rid of all custom imperative logic inside ViewManager.
We don't really need defaultView here.

This comment has been minimized.

@rigdern

rigdern Sep 7, 2017

Contributor

@shergin Are you saying we should default to passing nil to fontAttributes properties instead of forwarding whatever was in defaultView?

I think that will change the behavior but at least now things will be more consistent. For example, today I think if you don't specify fontSize during the initial render, the rendered fontSize will be RCTFont's default. If you later set the fontSize to 20 and then back to nil, I think it'll fallback to a default of defaultView.font.pointSize which is not necessarily the same as RCTFont's default so the font could be a different size now than it was when it was initially rendered.

@@ -41,6 +43,8 @@
@property (nonatomic, copy) RCTDirectEventBlock onContentSizeChange;
@property (nonatomic, copy) RCTDirectEventBlock onSelectionChange;
@property (readonly, nonatomic, strong) UIFont *font;

This comment has been minimized.

@shergin

shergin Sep 7, 2017

Contributor

Instad of having real prop, we probably want to have only setter which will redirect the value to backedTextInputView.font. (Also we have to add font to BackedTextInputProtocol if it does not have it already.)

rigdern added some commits Sep 21, 2017

Remove custom defaultView logic for font attribute props
I think this will change the behavior but at least now things will be more consistent. For example, today I think if you don't specify fontSize during the initial render, the rendered fontSize will be RCTFont's default. If you later set the fontSize to 20 and then back to nil, I think it'll fallback to a default of defaultView.font.pointSize which is not necessarily the same as RCTFont's default so the font could be a different size now than it was when it was initially rendered.
@rigdern

This comment has been minimized.

Contributor

rigdern commented Sep 21, 2017

@shergin Sorry for the delay. I pushed some changes that address your feedback.

@shergin

❤️
I wll try to land this in the begginnig of the week!
Thank you so much for your effort!

@@ -57,6 +58,8 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
_backedTextInput.textInputDelegate = self;
self.font = self.fontAttributes.font;

This comment has been minimized.

@shergin

shergin Sep 24, 2017

Contributor

Why do we need this?

This comment has been minimized.

@rigdern

rigdern Sep 24, 2017

Contributor

It ensures that _backedTextInput's font gets initialized to RCTFontAttributes's default font. This is not necessarily the same as _backedTextInput's default font.

@@ -70,6 +73,18 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
return _backedTextInput;
}
- (void)fontAttributesDidChangeWithFont:(UIFont *)font

This comment has been minimized.

@shergin

shergin Sep 24, 2017

Contributor

(We can consider moving this to superclass.)

This comment has been minimized.

@rigdern

rigdern Sep 24, 2017

Contributor

This overrides the superclass's implementation of fontAttributesDidChangeWithFont. Are you suggesting I move this logic to the superclass's implementation so RCTTextView doesn't have to override it?

This comment has been minimized.

@shergin

shergin Sep 24, 2017

Contributor

Yes. invalidateContentSize does not exist in base class, right? I think we can also move this method to the base class (even with possibly empty implementation).

This comment has been minimized.

@shergin

shergin Sep 24, 2017

Contributor

(This does not block landing. We can do it in the future.)

@facebook-github-bot

This comment has been minimized.

facebook-github-bot commented Sep 24, 2017

@shergin has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@rigdern rigdern deleted the rigdern:rigdern/ios-TextInput-allowFontScaling branch Nov 12, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment