react-native icon indicating copy to clipboard operation
react-native copied to clipboard

Text not fully rendered on iOS

Open soutua opened this issue 4 months ago • 30 comments

Description

We noticed an issue on our app that is using a lot of text (news articles), where in some specific cases the text is not fully rendered on iOS, after we moved into using the new architecture in our last release. I'm not sure what are all the factors that are needed for the issue to occur, but I was able to make a reproducer with some experimenting.

Steps to reproduce

Reproducer Snack can be found on: https://snack.expo.dev/@pekka.soutua/text-cut-off-on-ios Note that it's important to use the iPhone 16 Pro emulator, since the issue seems to be dependant on e.g. screen dimensions. I also tested that the issue reproduces on the issue reproducer template with the latest React Native version: https://github.com/soutua/react-native-new-architecture-text-rendering-issue

Scroll down the text until you see a paragraph with text "Consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit. This sentence is cut off.".

The sentence in the end "This sentence is cut off.", is not being fully rendered, the rest of it should be rendered on the next line but it isn't.

React Native Version

0.81.0

Affected Platforms

Runtime - iOS

Areas

Fabric - The New Renderer

Output of npx @react-native-community/cli info

Reproducible e.g. on the snack.expo.dev environment or on the issue reproducer template.

Stacktrace or Logs

-

MANDATORY Reproducer

https://snack.expo.dev/@pekka.soutua/text-cut-off-on-ios

Screenshots and Videos

Image

soutua avatar Aug 25 '25 15:08 soutua

Same here! But I noticed this occurs after opening the app for the second time.

Image

emirdeliz avatar Aug 25 '25 17:08 emirdeliz

We are seeing the same issue in our app (also when showing news articles)

johankasperi avatar Aug 28 '25 07:08 johankasperi

Can it be an issue with using lineHeight? Because when setting it to undefined in your repro https://github.com/soutua/react-native-new-architecture-text-rendering-issue/blob/b57f531407db0cc7380287294bdda626d81180b7/ReproducerApp/App.tsx#L319 I'm not seeing the bug. But when setting it to something else, texts is not fully rendered in one or several of the paragraphs.

johankasperi avatar Aug 28 '25 11:08 johankasperi

Can it be an issue with using lineHeight?

Yes I think it is a factor in this, I was not able to reproduce the issue either if I did not set the lineHeight.

Also other interesting observation during my reproduction experiments was that it's not about any single text paragraph in itself, but all the content in the screen can affect the reproduction of the issue. For example if you remove some of the text in my reproducer, the issue might go away.

soutua avatar Aug 28 '25 11:08 soutua

I think the issue is caused by these lines somehow. By removing paragraphystyles.minimumLineHeight all text is rendered. But they have the wrong lineheight.

johankasperi avatar Aug 28 '25 20:08 johankasperi

Doing something like this also seems to solve it in your repro and it produces the correct lineheight

    paragraphStyle.minimumLineHeight = lineHeight - 0.01;
    paragraphStyle.maximumLineHeight = lineHeight + 0.01;

but not sure if it solves it in all cases, for example is the 0.01 value arbitrary

johankasperi avatar Aug 28 '25 20:08 johankasperi

This code also fixes it

    paragraphStyle.lineHeightMultiple = round((lineHeight / font.lineHeight) * 10000.0) / 10000.0;

But this also feels unsafe. It doesn't work when I'm not rounding it

johankasperi avatar Aug 28 '25 20:08 johankasperi

Ended up doing this hack in my app. Probably just a workaround and not a real solution to the problem

    paragraphStyle.maximumLineHeight = lineHeight * 1.01;
    paragraphStyle.minimumLineHeight = lineHeight * 0.99;
    paragraphStyle.lineHeightMultiple = lineHeight / font.lineHeight;

johankasperi avatar Aug 29 '25 20:08 johankasperi

I am also facing the issue, not sure if it's exactly the same, but just in case: https://github.com/facebook/react-native/issues/53666

it's on android though. It happened only after updating to 0.81.x

do you guys face this issue on 0.80.x?

pierroo avatar Sep 09 '25 13:09 pierroo

do you guys face this issue on 0.80.x?

We encountered this issue also on 0.79, so might be a different issue (with similar symptoms).

soutua avatar Sep 09 '25 13:09 soutua

do you guys face this issue on 0.80.x?

We encountered this issue also on 0.79, so might be a different issue (with similar symptoms).

Our app (news articles) encountered this issue starting from 0.76 until 0.81. It seems this issue occurs based on the content and display size of the device.

renopp avatar Sep 12 '25 02:09 renopp

This bug makes it impossible to release an actual app at the moment considering how bad it is. Have you guys thought any other links to that? did any of you try @johankasperi suggestion for a fix, and did it work for you?

On my end the issue remains on android, haven't tested on iOS, but if this "hack" is confirmed to work I might dig deeper into android native files to find something similar.

Not sure if react native's team acknowledged this issue yet somewhere? It feels like a show stopper to me.

pierroo avatar Sep 15 '25 14:09 pierroo

I have been looking into this issue this week and it feels to me that this is a rounding error in calculating the line heights for the text being rendered, it would also somewhat explain why it also matters what other text is on the screen since the text layout y coordinates change, which can then affect the calculations and the rounding errors occurring.

So what I think happens is that here in this measureTextStorage function where the line heights are calculated, the calculated height is a bit too small, so the last line of the paragraph is missing fraction of the height that is required to render the text on the final line of the paragraph, so iOS decides to render it on the second last line, running off the screen. It also explains why @johankasperi's workaround of decreasing the minimum line height workarounds the issue, since it gives iOS's text renderer permission to render it with a bit less line height so that it fits on the last line even though it does have a bit less height.

I also found this code in the React Native's old architecture's code where apparently similar issue has been fixed 8 years ago.

I tested that doing a similar fix/workaround in the new architecture code where I do this here:

So instead of:

  size = (CGSize){
      ceil(size.width * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
      ceil(size.height * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};

add the epsilon:

  CGFloat epsilon = 0.001;
  size = (CGSize){
      ceil(size.width * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor + epsilon,
      ceil(size.height * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor + epsilon};

and the issue no longer reproduces, at least on my reproducer app. Not sure if this the best fix for this or not, but at least the fix is the same as in the old architecture's code.

Edit: Or actually I think this code makes more sense where the epsilon is added before the pixel snapping operation, since otherwise the pixel snapping is kinda pointless if the result is modified after:

  CGFloat epsilon = 0.001;
  size = (CGSize){
      ceil((size.width + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
      ceil((size.height + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};

soutua avatar Sep 16 '25 11:09 soutua

Or actually I think this code makes more sense where the epsilon is added before the pixel snapping operation, since otherwise the pixel snapping is kinda pointless if the result is modified after:

  CGFloat epsilon = 0.001;
  size = (CGSize){
      ceil((size.width + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
      ceil((size.height + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};

This seems to do the trick! Much better solution than my earlier attempts. Thank you @soutua!

johankasperi avatar Sep 29 '25 09:09 johankasperi

This seems to do the trick! Much better solution than my earlier attempts. Thank you @soutua!

We also released a version today with this change patched in. We were using the old architecture in the interim, but now moved back into using the new architecture with this patch. In our internal testing we were no longer able to reproduce the issue. But we'll see from the user (and several hundred angry journalists when they see words are missing from their articles) feedback if it's 100% fixed or not.

soutua avatar Sep 29 '25 10:09 soutua

I'm also experiencing this issue, only on iOS on new architecture.

@soutua I tried applying your fix (using patch-package), but the issue still persists for me. How did you apply your fix? The contents of my patch file look like this:

diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
index 216bb23..fe45309 100644
--- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
+++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
@@ -386,9 +386,10 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi
     size.height = enumeratedLinesHeight;
   }
 
+  CGFloat epsilon = 0.001;
   size = (CGSize){
-      ceil(size.width * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
-      ceil(size.height * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};
+      ceil((size.width + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
+      ceil((size.height + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};
 
   __block auto attachments = TextMeasurement::Attachments{};

Kitcheone avatar Oct 13 '25 14:10 Kitcheone

I'm also experiencing this issue, only on iOS on new architecture.

@soutua I tried applying your fix (using patch-package), but the issue still persists for me. How did you apply your fix? The contents of my patch file look like this:

diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
index 216bb23..fe45309 100644
--- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
+++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
@@ -386,9 +386,10 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi
     size.height = enumeratedLinesHeight;
   }
 
+  CGFloat epsilon = 0.001;
   size = (CGSize){
-      ceil(size.width * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
-      ceil(size.height * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};
+      ceil((size.width + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
+      ceil((size.height + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};
 
   __block auto attachments = TextMeasurement::Attachments{};

The patch seems correct to me at least. I'm applying the patch with yarn's patching feature (resolutions). Have you checked e.g. by less node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm that the lines have changed, so that the patch has applied correctly?

We've now been running the patch in production for couple of weeks and we've not gotten any feedback about the issue still occurring, so it seems to be fixed on our app with the patch. Although we are still using React Native 0.79 due to some issues with our dependencies with newer react native versions so we might not be running exactly the same code as you.

soutua avatar Oct 13 '25 14:10 soutua

@soutua Thanks for the reply. Yes, I can see that the lines have changed in my node_modules directory, so the patch has applied correctly, and yet it doesn't seem to fix the issue. I'm currently running on React Native 0.81.4 (via Expo 54), so yes there could be something else at play.

I'll keep digging and report back here if I manage to resolve it!

Kitcheone avatar Oct 13 '25 15:10 Kitcheone

@soutua FYI, your fix does indeed work against RN 0.79.5. So it seems like there's a further problem with RN 0.81, which I haven't been able to fix yet. I've tried larger values of epsilon (as a test), but it still always truncates.

Note that the code in RCTTextLayoutManager in RN is a little bit different in 0.79, so the patch I generated looks like this:

diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
index f75f78f..98b8afd 100644
--- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
+++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
@@ -373,7 +373,8 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi
     size.width = textContainer.size.width;
   }
 
-  size = (CGSize){RCTCeilPixelValue(size.width), RCTCeilPixelValue(size.height)};
+  CGFloat epsilon = 0.001;
+  size = (CGSize){RCTCeilPixelValue(size.width + epsilon), RCTCeilPixelValue(size.height + epsilon)};
 
   __block auto attachments = TextMeasurement::Attachments{};

Kitcheone avatar Oct 15 '25 09:10 Kitcheone

Yep the lines are a bit different in the 0.79, with the same functionality though.

It's interesting if the fix doesn't work in RN 0.81, since the reproducer app that is linked in the issue description is using 0.81, and the issue was fixed also in the reproducer app with the patch. But could of course be that just that it just made it less likely to happen 🤔

soutua avatar Oct 15 '25 10:10 soutua

I'm also experiencing this issue on React Native 0.81.4 with the new architecture on iOS. Can confirm that the proposed fix doesn't seem to work on 0.81.4 as mentioned by @Kitcheone.

This is a critical issue for apps with text-heavy content. Hoping this gets resolved soon.

Unuuuuu avatar Oct 21 '25 16:10 Unuuuuu

I now tested our app with RN version 0.81.5 with and without this change:

  CGFloat epsilon = 0.001;
  size = (CGSize){
      ceil((size.width + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
      ceil((size.height + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};

If I don't have that patch included, I can reproduce the issue quite easily, maybe in 50% of our app's news articles. If I use the patch, I haven't yet been able to reproduce it, I checked maybe 50 articles when testing. I'm wondering if the issue you are encountering might possibly be some other issue? Since to me it seems like the patch works similarly as it did in the RN 0.79 we are using in production 🤔

soutua avatar Oct 23 '25 15:10 soutua

One thing that came to mind; check that you are not using the pre-compiled React Native iOS binaries. That option came in RN 0.81. If you are using the pre-compiled version, then the patch is not used.

soutua avatar Oct 23 '25 15:10 soutua

@soutua that's a really good point! We're using Expo, and so we were trying RN 0.81 with Expo SDK54, which does use precompiled RN binaries by default. I'll try disabling it.

Kitcheone avatar Oct 23 '25 15:10 Kitcheone

@soutua Thank you for pointing that out! As you mentioned, we were indeed using the precompiled RN binary. After enabling the buildReactNativeFromSource option to build RN from source instead of using the precompiled binary, we can confirm that the issue has been resolved in all the places where it was previously reproducible.

Unuuuuu avatar Oct 24 '25 04:10 Unuuuuu

I can also confirm that the patch seems to fix the problem on RN 0.81, once we enabled buildReactNativeFromSource.

@soutua I think we should raise a PR with your fix. As the exact same fix used to exist on old arch, I would hope that it would get approved and merged. Do you want to raise it? If not, I'm happy to raise it.

Kitcheone avatar Oct 24 '25 12:10 Kitcheone

Yep, wanted to make sure that no one here is still able to reproduce the issue when using the patch, but I've now made a PR of it: https://github.com/facebook/react-native/pull/54260

soutua avatar Oct 24 '25 13:10 soutua

@soutua Nice one - great stuff. Let's hope it gets merged!

Kitcheone avatar Oct 24 '25 13:10 Kitcheone

@soutua Thanks for the suggestion. Since applying this patch, and after releasing the app a couple of weeks ago, our journalists and users have stopped reporting the issue of text cutoff on the iOS platform. 🫡

  CGFloat epsilon = 0.001;
  size = (CGSize){
      ceil((size.width + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
      ceil((size.height + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};

renopp avatar Nov 03 '25 10:11 renopp

Yep, wanted to make sure that no one here is still able to reproduce the issue when using the patch, but I've now made a PR of it: #54260

https://github.com/facebook/react-native/pull/54510 Looks like PR got reverted, any reason for that?

KestasVenslauskas avatar Dec 19 '25 09:12 KestasVenslauskas