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

<Text> Component with over ~500 lines won't render [iOS] #19453

Closed
3 tasks done
jackthias opened this issue May 25, 2018 · 19 comments
Closed
3 tasks done

<Text> Component with over ~500 lines won't render [iOS] #19453

jackthias opened this issue May 25, 2018 · 19 comments
Labels
Bug Platform: iOS iOS applications. Priority: Mid Resolution: Locked This issue was locked by the bot.

Comments

@jackthias
Copy link

jackthias commented May 25, 2018

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 {
  Text,
  View,
  ScrollView
} from 'react-native';

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

export default class App extends Component {
  render() {
    return (
      <View style={{
        flex: 1,
        justifyContent: 'flex-start',
        alignItems: 'center',
        backgroundColor: '#F5FCFF',
      }}>
        <ScrollView style={{flex: 1}}>
          <Text>{text}</Text>
        </ScrollView>
      </View>
    );
  }
}

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

@jackthias jackthias changed the title <Text> Component with over ~500 lines won <Text> Component with over ~500 lines won't render May 25, 2018
@jackthias jackthias changed the title <Text> Component with over ~500 lines won't render <Text> Component with over ~500 lines won't render [iOS] May 25, 2018
@react-native-bot

This comment has been minimized.

@jackthias
Copy link
Author

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

@jackthias
Copy link
Author

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.

@stale
Copy link

stale bot commented Sep 30, 2018

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.

@jtnix
Copy link

jtnix commented Nov 20, 2018

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

@ericlewis
Copy link
Contributor

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

@ericlewis
Copy link
Contributor

ericlewis commented Feb 28, 2019

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

@ericlewis
Copy link
Contributor

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.

@CatapultJesse
Copy link

Related: #22713

@jackthias
Copy link
Author

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.

@jackthias
Copy link
Author

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.

@rickhanlonii
Copy link
Member

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

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

@jenni-divvito
Copy link

@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

@janicduplessis
Copy link
Contributor

janicduplessis commented Apr 9, 2019

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.

@janicduplessis
Copy link
Contributor

janicduplessis commented Apr 10, 2019

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.

@mikelovesrobots
Copy link

@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

@janicduplessis
Copy link
Contributor

@mikelovesrobots You can try #24387, it should fix the issue completely!

@mikelovesrobots
Copy link

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

kelset pushed a commit that referenced this issue Jun 28, 2019
Summary:
The current technique we use to draw text uses linear memory, which means that when text is too long the UIView layer is unable to draw it. This causes the issue described [here](#19453). On an iOS simulator the bug happens at around 500 lines which is quite annoying. It can also happen on a real device but requires a lot more text.

To be more specific the amount of text doesn't actually matter, it is the size of the UIView that we use to draw the text. When we use `[drawRect:]` the view creates a bitmap to send to the gpu to render, if that bitmap is too big it cannot render.

To fix this we can use `CATiledLayer` which will split drawing into smaller parts, that gets executed when the content is about to be visible. This drawing is also async which means the text can seem to appear during scroll. See https://developer.apple.com/documentation/quartzcore/calayer?language=objc.

`CATiledLayer` also adds some overhead that we don't want when rendering small amount of text. To fix this we can use either a regular `CALayer` or a `CATiledLayer` depending on the size of the view containing the text. I picked 1024 as the threshold which is about 1 screen and a half, and is still smaller than the height needed for the bug to occur when using a regular `CALayer` on a iOS simulator.

Also found this which addresses the problem in a similar manner and took some inspiration from the code linked there GitHawkApp/StyledTextKit#14 (comment)

Fixes #19453

## Changelog

[iOS] [Fixed] - Use CALayers to draw text, fixes rendering for long text
Pull Request resolved: #24387

Test Plan:
- Added the example I was using to verify the fix to RNTester.
- Made sure all other examples are still rendering properly.
- Tested text selection

Reviewed By: shergin

Differential Revision: D15918277

Pulled By: sammy-SC

fbshipit-source-id: c45409a8413e6e3ad272be39ba527a4e8d349e28
M-i-k-e-l pushed a commit to M-i-k-e-l/react-native that referenced this issue Mar 10, 2020
Summary:
The current technique we use to draw text uses linear memory, which means that when text is too long the UIView layer is unable to draw it. This causes the issue described [here](facebook#19453). On an iOS simulator the bug happens at around 500 lines which is quite annoying. It can also happen on a real device but requires a lot more text.

To be more specific the amount of text doesn't actually matter, it is the size of the UIView that we use to draw the text. When we use `[drawRect:]` the view creates a bitmap to send to the gpu to render, if that bitmap is too big it cannot render.

To fix this we can use `CATiledLayer` which will split drawing into smaller parts, that gets executed when the content is about to be visible. This drawing is also async which means the text can seem to appear during scroll. See https://developer.apple.com/documentation/quartzcore/calayer?language=objc.

`CATiledLayer` also adds some overhead that we don't want when rendering small amount of text. To fix this we can use either a regular `CALayer` or a `CATiledLayer` depending on the size of the view containing the text. I picked 1024 as the threshold which is about 1 screen and a half, and is still smaller than the height needed for the bug to occur when using a regular `CALayer` on a iOS simulator.

Also found this which addresses the problem in a similar manner and took some inspiration from the code linked there GitHawkApp/StyledTextKit#14 (comment)

Fixes facebook#19453

## Changelog

[iOS] [Fixed] - Use CALayers to draw text, fixes rendering for long text
Pull Request resolved: facebook#24387

Test Plan:
- Added the example I was using to verify the fix to RNTester.
- Made sure all other examples are still rendering properly.
- Tested text selection

Reviewed By: shergin

Differential Revision: D15918277

Pulled By: sammy-SC

fbshipit-source-id: c45409a8413e6e3ad272be39ba527a4e8d349e28
@CyxouD
Copy link

CyxouD commented Mar 26, 2020

@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 (#18132 (comment)) 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>

@facebook facebook locked as resolved and limited conversation to collaborators Jun 24, 2020
@react-native-bot react-native-bot added the Resolution: Locked This issue was locked by the bot. label Jun 24, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug Platform: iOS iOS applications. Priority: Mid Resolution: Locked This issue was locked by the bot.
Projects
None yet
Development

Successfully merging a pull request may close this issue.