react-native: Component with over ~500 lines won't render [iOS]

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

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 19 (7 by maintainers)

Commits related to this issue

Most upvoted comments

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.

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

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