Skip to content
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

fix: Improve text line height calculation to properly align text on iOS #46884

Open
wants to merge 36 commits into
base: main
Choose a base branch
from

Conversation

ArekChr
Copy link

@ArekChr ArekChr commented Oct 8, 2024

Summary:

This pull request addresses an issue in React Native on iOS: Text gets truncated when the line height is too small, even if the line-height matches or exceeds the font size.

The existing logic did not correctly handle font metrics (ascent, descent, top, bottom) when the total height exceeded the line height. The implementation now ensures a more balanced vertical space distribution, reducing text clipping and misalignment.

The fix mirrors a similar solution made on Android, ensuring consistency across both platforms.

For more context, see the related issue: React Native issue #29507.

Changelog:

[IOS] [FIXED] - Fixed an issue where text clipping and misalignment occurred when the line-height was smaller than the total ascent and descent of the font.

Test Plan:

The result is an improvement over the current implementation, reducing the occurrence of text truncation. While minimal truncation still exists at single-line heights, particularly with special characters, the implementation better preserves the vertical alignment of the text compared to previous behavior. However, like the Android implementation, this does not fully resolve the issue of React Native truncating content that is out of bounds of the span.

Old Arch Before Old Arch After
Old Arch Before Old Arch After
New Arch Before New Arch After
New Arch Before New Arch After

@facebook-github-bot
Copy link
Contributor

Hi @ArekChr!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at [email protected]. Thanks!

@ArekChr ArekChr changed the title fix: Improve text line height calculation to properly align text fix: Improve text line height calculation to properly align text on iOS Oct 8, 2024
@facebook-github-bot
Copy link
Contributor

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@facebook-github-bot facebook-github-bot added CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Oct 8, 2024
contentFrame:(CGRect)contentFrame {
UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL];
if (!font) {
font = [UIFont systemFontOfSize:14];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn’t find systemFontSize used anywhere else in the repo, and the default font size of 14 has been hardcoded. Also, since systemFontSize is not available on tvOS, I’m not sure if it would be a suitable replacement. Let me know your thoughts on whether we should stick with the hardcoded value for consistency or if there’s a preferred approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NickGerleman

This seems sketch. E.g. what if there is a font multiplier applied?

Comment on lines 120 to 127
CGFloat verticalOffset = 0;
if (textHeight > lineHeight) {
CGFloat difference = textHeight - lineHeight;
verticalOffset = difference / 2.0;
} else if (textHeight < lineHeight) {
CGFloat difference = lineHeight - textHeight;
verticalOffset = -(difference / 2.0);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think comment explaining the calculations might be useful

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, added comments

@mrousavy
Copy link
Contributor

mrousavy commented Oct 8, 2024

nice!

@ArekChr ArekChr force-pushed the improve-lineheight-calc-ios branch from 246c550 to f998a9a Compare October 9, 2024 07:48
Copy link
Contributor

@cipolleschi cipolleschi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes looks good. We discussed this internally and we can proceed importing it.

To set the expectation, this might take some time to merge, given that this might affect internal tests and product code and we would need some time to fix them. But thankfully, we have a feature flag to control the rollout! 😄

@facebook-github-bot
Copy link
Contributor

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

@cipolleschi
Copy link
Contributor

@ArekChr CI is currently red. The error is

[React-RCTText] Compiling RCTTextView.mm
Error: use of undeclared identifier 'ReactNativeFeatureFlags'; did you mean 'facebook::react::ReactNativeFeatureFlags'?
  if (ReactNativeFeatureFlags::enableLineHeightCentering()) {

Could you please verify why this is happening and fix it?
You should be able to build it locally running these commands in the react-native root folder:

yarn install
cd packages/rn-tester
bundle install
bundle exec pod install
open RNTesterPods.xcworkspace

and then build from Xcode.

If you need to run the old architecture, you can disable it by adding:

- (BOOL) newArchEnabled
{
  return NO;
}

in the AppDelegate.mm file

@ArekChr
Copy link
Author

ArekChr commented Oct 9, 2024

Hi @cipolleschi, the issue has been fixed, and the build runs successfully locally. Thanks!

@cipolleschi
Copy link
Contributor

We were discussing internally that it would be better to have two separate feature flags, one for Android and one for iOS. Could I ask you to:

  1. revert the changes you made on the preexisting feature flag, which was targeting only Android
  2. add the iOS dual of that feature flag

The idea is to be able to control the two platforms separatedly.

Thank you so much for your help and patience

@ArekChr
Copy link
Author

ArekChr commented Oct 11, 2024

@cipolleschi I have an issue where the ReactNativeFeatureFlags.h header is not found when using dynamic frameworks. I’ve tried a few things but haven’t resolved the issue. Do you have any suggestions on how to fix or work around this?

@ArekChr ArekChr force-pushed the improve-lineheight-calc-ios branch from fa44b24 to 9e5ed70 Compare October 14, 2024 07:01
@ArekChr
Copy link
Author

ArekChr commented Oct 22, 2024

Hi, @cipolleschi. The CI failed due to some issues unrelated to my changes. I’ve likely fixed the problem with dynamic frameworks, but two workflows seem missing. Could you please trigger those workflows again so we can proceed?

@ArekChr
Copy link
Author

ArekChr commented Dec 17, 2024

@cipolleschi Fixed

@facebook-github-bot
Copy link
Contributor

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

@facebook-github-bot
Copy link
Contributor

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

@facebook-github-bot
Copy link
Contributor

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

1 similar comment
@facebook-github-bot
Copy link
Contributor

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

@LJNGDAHL
Copy link

LJNGDAHL commented Jan 7, 2025

Any progress on this? 👀

@facebook-github-bot
Copy link
Contributor

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

@cipolleschi
Copy link
Contributor

Sorry if it is taking a bit. I'm trying to get an internal review on this.

@ludvigbartholdsson
Copy link

Awesome if we could get this PR merged (if it works and passes tests ofc) @cipolleschi do you think it's possible next week?

@facebook-github-bot
Copy link
Contributor

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

@ludvigbartholdsson
Copy link

Hi @cipolleschi , do you have a new status on this?

@@ -98,6 +99,42 @@ - (void)setTextStorage:(NSTextStorage *)textStorage
[self setNeedsDisplay];
}

- (CGPoint)calculateDrawingPointWithTextStorage:(NSTextStorage *)textStorage
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NickGerleman

It would be best to keep the changes to the Fabric component instead of this one.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you point me to the specific place in the Fabric component where I should move this change?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! I'm a developer following this thread. Does this suggestion mean that this fix will not be made for the old architecture? I've been following the original ticket for years, and such outcome would be quite disappointing 😕 My team is not considering upgrading to Fabric any time soon (due to many old arch dependencies, and also performance/bundle size/reliability concerns).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ArekChr I believe that the suggestion here was to not touch the files in the old architecture. The PR already updates the New Architecture, so we are good.

@SimpleCreations Thanks for the feedback, I believe that in this case we can keep the fix here. As a general thought, though.. Yes, in the future the Old Architecture will not be maintained.

Right now, we already released 0.76 and 0.77 where the New Architecture is the default. We are already working on 0.78 and once that's out, 0.75 will go out of support. That is the last version where the Old Architecture is used by default.

Consider that new features are only developed for the New Architecture and many libraries will be only compatible with the New Architecture, moving forward (reanimated and react-native-vision-camera to mention two of them). So we strongly advise to start migrating to the New Architecture.

I'd be curious to know what is holding you back in more details. If you can replicate Performance and bundle issues in separate reproducers, we are more than happy to look into them as soon as possible.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cipolleschi Thank you for reconsidering! I appreciate the decision to keep the fix very much.

I'll try to answer your question about what is holding my team back. The primary reason is we use quite a few SaaS solutions that provide their own React Native SDKs that are old arch only, and they have no timeline of adding new arch support. Also we have developed a few of our own React Native wrappers for other SaaS solutions, and they are shared between other apps in our company, which are also on old arch.

Performance and bundle size concerns come from me following this thread: #47490 where multiple people share negative experiences with new arch migration, but I'm not sure if this would affect us, because we haven't tried migrating yet (due to what I described above). So please disregard this part for now 🙂

I would like to also mention that the same fix for line height has been made for old arch on Android here: #46362, so it would make sense to include an old arch fix on iOS too.

return contentFrame.origin;
}

UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NickGerleman

Do you know if this is influenced by the full set of text attributes on the Text component? Like, if this is italicized, will it pick up that version of the font?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code will return the correct font if the entire text is consistently styled (e.g., italic or bold). However, if the text contains different styles in different parts (e.g., a mix of italic and bold), this code will only reflect the style of the first character.

contentFrame:(CGRect)contentFrame {
UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL];
if (!font) {
font = [UIFont systemFontOfSize:14];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NickGerleman

This seems sketch. E.g. what if there is a font multiplier applied?

CGFloat lineHeight = font.lineHeight;

if (paragraphStyle && paragraphStyle.minimumLineHeight > 0) {
lineHeight = paragraphStyle.minimumLineHeight;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NickGerleman

Why do we do this work when an explicit lineHeight is not set?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve inverted the condition to make it clearer and ensure we exit early when minimumLineHeight isn’t set.

@cipolleschi
Copy link
Contributor

I am sorry that this is taking so long, but this diff is very delicate to land as it touches the very core of text rendering and it can introduce a lot of regression. That's why it is take more time.
On top of that, there is further feedback that needs to be addressed and that i forwarded from internal colleagues.

@cipolleschi
Copy link
Contributor

need a rebase due to the feature flags. 😢

@ludvigbartholdsson
Copy link

Hi, how is this going?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants