React-native: <Text> Component with over ~500 lines won't render [iOS]

Created on 25 May 2018  路  19Comments  路  Source: facebook/react-native


A Text component with about 500-600+ lines of text renders completely blank.

Environment

Environment:
OS: macOS Sierra 10.12.6
Node: 9.3.0
Yarn: Not Found
npm: 5.8.0
Watchman: 4.9.0
Xcode: Xcode 9.2 Build version 9C40b
Android Studio: 3.0 AI-171.4443003

Packages: (wanted => installed)
react: 16.3.1 => 16.3.1
react-native: 0.55.4 => 0.55.4

Steps to Reproduce

```import React, { Component } from 'react';
import {
Text,
View,
ScrollView
} from 'react-native';

const text = ".\n".repeat(600)

export default class App extends Component {
render() {
return (
flex: 1,
justifyContent: 'flex-start',
alignItems: 'center',
backgroundColor: '#F5FCFF',
}}>
{text}


);
}
}
```

At 600 lines there's no rendering of the text component, at 200 it renders fine.

Expected Behavior

I expected the normal behavior of a Text component embedded in a ScrollView. For an example, render the above code with a repeat value of 200 instead of 600.

screen shot 2018-05-25 at 2 38 33 pm

Actual Behavior

Nothing is rendered, just a blank area where the text should be.

screen shot 2018-05-25 at 2 37 58 pm

Bug Mid-Pri iOS Locked

Most helpful comment

As a workaround using

<TextInput multiline editable={false}>
  Some very long text
</TextInput>

works fine.

I've looked into it a bit and we don't seem to be using UILabel. We are using NSLayoutManager to draw the text inside a UIView subclass. Not sure what magic UITextView does to be able to render this properly.

I've also noticed that the issue does not happen on a physical device (iPhone XS), or maybe the number of lines required to trigger the bug is bigger.

All 19 comments

It looks like your issue may be missing some necessary information. Can you run react-native info and edit your issue to include these results under the Environment section?

Sorry, I accidentally tapped the enter key while I was still typing the title.

This is tagged as no environment info, but there's clearly environment info in the post. Am I missing anything? That's the output of the command.

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as "For Discussion" or "Good first issue" and I will leave it open. Thank you for your contributions.

Still having this issue in RN 0.57.

Had to write a chunking routine to break large strings into 10k chunks and rendered in sequential Text components, which gets around the issue for us.

Does not happen on Android builds, only IOS, including hardware 6, X and XR

The magic number appears to be 490 characters. Am taking a look at this.

~Seems to be a bug in Yoga.~
~creating an NSTextContainer with a height that is taller than 8203 causes things to not be rendered 馃~

I can't tell if it is Yoga or NSTextContainer, but the new magic number is 8192. You cannot return a height greater than 8192 from RCTTextShadowViewMeasure when the height of an NSTextContainer is greater than 8203. If you limit the measure to max out at 8192 artificially, it will draw (but is obviously not all of the text). This is puzzling, as I cannot tell who is really at fault. It may make sense to try this with a standalone iOS project.

Hey, I haven't touched this in a while, because I found a workaround.

Here's what I found while digging into the issue way back when I discovered this issue. It appears to be a problem with the Cocoa UILabel Component for iOS which is intended only for short blocks of text. This is the component that the React Native uses to implement the component on iOS. Which is generally fine, until you run into large rendering of text, in which case the Label no longer suffices.

In a native iOS application, this problem would be avoided by the use of a read-only UITextView, which--unlike UILabel--is built to support large texts. You can get your RN project to use the UITextView object instead of the UILabel object by using instead of and setting the appropriate props to make it read-only (and not select-able, if that matters).

The issue I ran into was that the component doesn't (or at least didn't, it'd been many months since I've checked) have the scroll methods that the component had. I solved this by instead using a list of s instead and artificially breaking up the text. Though this workaround was lackluster.

Hope this helps anyone looking for workarounds or anyone trying to solve this issue. I would recommend adding a iOS-specific prop that allows the developer to specify which text object to use (probably phrased in a more abstract manner, like long-text?). Or even automatically switching when the length of the label would exceed more than a dozen or so lines (the TextView is more performant for long texts anyways). I'd also suggest implementing scroll methods for component.

As a quick clarification, this would happen even if you stripped React Native away and just had a vanilla-iOS app built with Swift and UIKit display hundreds of lines of text in a UILabel. UILabels are not intended for large text, Apple's documentation suggests as much. The issue at play is just that React Native's categories for text components don't perfectly align with Apple's categories for text objects.

@jackthias good clarification, it sounds like the action here should be to:

  • document the limitation
  • add warnings in dev if the limit is passed

@jackthias - hi - I'm just curious what you mean by "You can get your RN project to use the UITextView object instead of the UILabel object by using instead of and setting the appropriate props to make it read-only (and not select-able, if that matters)"

I went looking for a read only property on the <Text/> component https://facebook.github.io/react-native/docs/text but couldn't find anything

As a workaround using

<TextInput multiline editable={false}>
  Some very long text
</TextInput>

works fine.

I've looked into it a bit and we don't seem to be using UILabel. We are using NSLayoutManager to draw the text inside a UIView subclass. Not sure what magic UITextView does to be able to render this properly.

I've also noticed that the issue does not happen on a physical device (iPhone XS), or maybe the number of lines required to trigger the bug is bigger.

So after investigating this for a very long time I managed to reduce the issue to the usage of [drawRect:] and a frame with either width or height greater than 5000 (the exact number is between 5k and 6k). Seems like a bug in UIKit, here's a repro:

@interface TestView : UIView

@end

@implementation TestView

- (void)drawRect:(CGRect)rect
{
  [[UIColor redColor] setFill];
  UIRectFill(CGRectMake(0, 0, 100, 100));
}

@end

...

 // as soon as one of these is bigger than 5k the view no longer renders.
TestView *view = [[TestView alloc] initWithFrame:CGRectMake(0, 0, 5000, 6000)];
view.backgroundColor = [UIColor whiteColor];
[rootViewController.view addSubview:view];

This example would result in a fully black screen (even the white background doesn't render). Removing drawRect from TestView would cause the view to start rendering properly again (cover the screen as a white view). Reducing width and height to 5k would cause everything to work again (white screen with red square).

I tried exploring a bunch of workarounds and managed to find a solution that works! I noticed that text rendering works when using a CATextLayer instead of drawRect. I also went and looked at how ComponentKit renders text and found that it uses a custom CALayer subclass and draws the text using NSLayoutManager in drawInContext (https://github.com/facebook/componentkit/blob/master/ComponentTextKit/CKTextComponentLayer.mm#L100, https://github.com/facebook/componentkit/blob/master/ComponentTextKit/TextKit/CKTextKitRenderer.mm#L101) . This is very similar to the setup we have in RCTTextView.m and I managed to get a proof of concept working and rendering text properly.

Looking into cleaning this up and opening a PR to fix this.

Edit: This doesn't actually fixes the bug but increases the amount of lines that can be rendered. The number of lines required to trigger the bug also seem to depend on the device. For the simulator the number is around 500 lines, for an iPhone XS the number is around 85k lines. Using CALayer seems to 2-3x the number of lines that can be rendered.

Edit 2: Got a fully working solution using CATiledLayer.

@janicduplessis It's awesome that you're working on this. I'd love to help but I'm afraid the iOS internal rendering is above my pay grade. If there's something else I can do, let me know.


<TextInput multiline editable={false}>
  Some very long text
</TextInput>

The TextInput workaround only works as long as the scrollEnabled prop is left on, as in the default. If you turn off vertical scrolling to better emulate a Text component you're back with the disappearing text.


Also, it being tied to the height of the Text component seems to fit what I'm seeing here where in the accessibility menu of iOS, if you bump up the font size to the max -- as you would if you were vision impaired -- lots more Text components disappear than at default system font sizes.

System font maxed out | Default
--- | ---
Screenshot 2019-04-12 09 22 58 | Screenshot 2019-04-12 09 59 31

@mikelovesrobots You can try https://github.com/facebook/react-native/pull/24387, it should fix the issue completely!

It sure does. I left a comment over there with comparison screenshots.

@janicduplessis thank you for your solution.
For those that experience also content not scrolling on Android because of custom font, I combined this solution with this fix (https://github.com/facebook/react-native/issues/18132#issuecomment-499267942) in separate component:
https://gist.github.com/CyxouD/bb42999e066cb7518768c1c29bb1b799.
Use it like this:

<ScrollWithCustomFontFixedTextInput
            style={[styles.introText, { marginBottom: 40 }]}
            initialHeight={Dimensions.get('window').height}
            width={Dimensions.get('window').width - 20 * 2}>
            {i18n.t('ageVerificationIntro.labelUserAgreement')}
</ScrollWithCustomFontFixedTextInput>
Was this page helpful?
0 / 5 - 0 ratings