Skip to content

Commit b68966e

Browse files
janicduplessiskelset
authored andcommittedJun 28, 2019
Use CALayers to draw text (#24387)
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
1 parent 99bc31c commit b68966e

File tree

6 files changed

+222
-31
lines changed

6 files changed

+222
-31
lines changed
 

‎Libraries/Text/RCTText.xcodeproj/project.pbxproj

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
19461666225DC3B300E4E008 /* RCTTextRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = 19461664225DC3B300E4E008 /* RCTTextRenderer.m */; };
1011
5956B130200FEBAA008D9D16 /* RCTRawTextShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5956B0FD200FEBA9008D9D16 /* RCTRawTextShadowView.m */; };
1112
5956B131200FEBAA008D9D16 /* RCTRawTextViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5956B0FE200FEBA9008D9D16 /* RCTRawTextViewManager.m */; };
1213
5956B132200FEBAA008D9D16 /* RCTSinglelineTextInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5956B101200FEBA9008D9D16 /* RCTSinglelineTextInputView.m */; };
@@ -187,6 +188,8 @@
187188
/* End PBXCopyFilesBuildPhase section */
188189

189190
/* Begin PBXFileReference section */
191+
19461664225DC3B300E4E008 /* RCTTextRenderer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextRenderer.m; sourceTree = "<group>"; };
192+
19461665225DC3B300E4E008 /* RCTTextRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextRenderer.h; sourceTree = "<group>"; };
190193
2D2A287B1D9B048500D4039D /* libRCTText-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRCTText-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
191194
58B5119B1A9E6C1200147676 /* libRCTText.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTText.a; sourceTree = BUILT_PRODUCTS_DIR; };
192195
5956B0F9200FEBA9008D9D16 /* RCTConvert+Text.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+Text.h"; sourceTree = "<group>"; };
@@ -360,6 +363,8 @@
360363
children = (
361364
5956B129200FEBAA008D9D16 /* NSTextStorage+FontScaling.h */,
362365
5956B125200FEBAA008D9D16 /* NSTextStorage+FontScaling.m */,
366+
19461665225DC3B300E4E008 /* RCTTextRenderer.h */,
367+
19461664225DC3B300E4E008 /* RCTTextRenderer.m */,
363368
5956B126200FEBAA008D9D16 /* RCTTextShadowView.h */,
364369
5956B122200FEBAA008D9D16 /* RCTTextShadowView.m */,
365370
5956B123200FEBAA008D9D16 /* RCTTextView.h */,
@@ -439,6 +444,7 @@
439444
developmentRegion = English;
440445
hasScannedForEncodings = 0;
441446
knownRegions = (
447+
English,
442448
en,
443449
);
444450
mainGroup = 58B511921A9E6C1200147676;
@@ -504,6 +510,7 @@
504510
5956B142200FEBAA008D9D16 /* RCTTextViewManager.m in Sources */,
505511
5956B135200FEBAA008D9D16 /* RCTBaseTextInputView.m in Sources */,
506512
5956B144200FEBAA008D9D16 /* RCTVirtualTextViewManager.m in Sources */,
513+
19461666225DC3B300E4E008 /* RCTTextRenderer.m in Sources */,
507514
5C245F39205E216A00D936E9 /* RCTInputAccessoryShadowView.m in Sources */,
508515
5956B13B200FEBAA008D9D16 /* RCTMultilineTextInputViewManager.m in Sources */,
509516
5956B134200FEBAA008D9D16 /* RCTSinglelineTextInputViewManager.m in Sources */,

‎Libraries/Text/Text/RCTTextRenderer.h

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <UIKit/UIKit.h>
9+
10+
NS_ASSUME_NONNULL_BEGIN
11+
12+
/**
13+
* Used by text layers to render text. Note that UIKit crashes if this delegate is implemented
14+
* directly on a UIView subclass since it already implements it for the view's root
15+
* layer. This is why this is implemented in a separate class.
16+
*/
17+
@interface RCTTextRenderer : NSObject <CALayerDelegate>
18+
19+
- (void)setTextStorage:(NSTextStorage *)textStorage contentFrame:(CGRect)contentFrame;
20+
21+
@end
22+
23+
NS_ASSUME_NONNULL_END

‎Libraries/Text/Text/RCTTextRenderer.m

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTTextRenderer.h"
9+
10+
#import "RCTTextAttributes.h"
11+
12+
@implementation RCTTextRenderer
13+
{
14+
NSTextStorage *_Nullable _textStorage;
15+
CGRect _contentFrame;
16+
}
17+
18+
- (void)setTextStorage:(NSTextStorage *)textStorage
19+
contentFrame:(CGRect)contentFrame
20+
{
21+
_textStorage = textStorage;
22+
_contentFrame = contentFrame;
23+
}
24+
25+
- (void)drawLayer:(CALayer *)layer
26+
inContext:(CGContextRef)ctx;
27+
{
28+
if (!_textStorage) {
29+
return;
30+
}
31+
32+
CGRect boundingBox = CGContextGetClipBoundingBox(ctx);
33+
CGContextSaveGState(ctx);
34+
UIGraphicsPushContext(ctx);
35+
36+
NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject;
37+
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
38+
39+
NSRange glyphRange =
40+
[layoutManager glyphRangeForBoundingRect:boundingBox
41+
inTextContainer:textContainer];
42+
43+
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:_contentFrame.origin];
44+
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:_contentFrame.origin];
45+
46+
UIGraphicsPopContext();
47+
CGContextRestoreGState(ctx);
48+
}
49+
50+
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
51+
{
52+
// Disable all implicit animations.
53+
return (id)[NSNull null];
54+
}
55+
56+
@end

‎Libraries/Text/Text/RCTTextView.m

+113-31
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,36 @@
1313
#import <React/UIView+React.h>
1414

1515
#import "RCTTextShadowView.h"
16+
#import "RCTTextRenderer.h"
17+
18+
@interface RCTTextTiledLayer : CATiledLayer
19+
20+
@end
21+
22+
@implementation RCTTextTiledLayer
23+
24+
+ (CFTimeInterval)fadeDuration
25+
{
26+
return 0.05;
27+
}
28+
29+
@end
1630

1731
@implementation RCTTextView
1832
{
19-
CAShapeLayer *_highlightLayer;
2033
UILongPressGestureRecognizer *_longPressGestureRecognizer;
2134

2235
NSArray<UIView *> *_Nullable _descendantViews;
2336
NSTextStorage *_Nullable _textStorage;
2437
CGRect _contentFrame;
38+
RCTTextRenderer *_renderer;
39+
// For small amount of text avoid the overhead of CATiledLayer and
40+
// make render text synchronously. For large amount of text, use
41+
// CATiledLayer to chunk text rendering and avoid linear memory
42+
// usage.
43+
CALayer *_Nullable _syncLayer;
44+
RCTTextTiledLayer *_Nullable _asyncTiledLayer;
45+
CAShapeLayer *_highlightLayer;
2546
}
2647

2748
- (instancetype)initWithFrame:(CGRect)frame
@@ -31,6 +52,7 @@ - (instancetype)initWithFrame:(CGRect)frame
3152
self.accessibilityTraits |= UIAccessibilityTraitStaticText;
3253
self.opaque = NO;
3354
self.contentMode = UIViewContentModeRedraw;
55+
_renderer = [RCTTextRenderer new];
3456
}
3557
return self;
3658
}
@@ -65,6 +87,7 @@ - (void)reactSetFrame:(CGRect)frame
6587
// This disables the frame animation, without affecting opacity, etc.
6688
[UIView performWithoutAnimation:^{
6789
[super reactSetFrame:frame];
90+
[self configureLayer];
6891
}];
6992
}
7093

@@ -91,55 +114,101 @@ - (void)setTextStorage:(NSTextStorage *)textStorage
91114
[self addSubview:view];
92115
}
93116

94-
[self setNeedsDisplay];
117+
[_renderer setTextStorage:textStorage contentFrame:contentFrame];
118+
[self configureLayer];
119+
[self setCurrentLayerNeedsDisplay];
95120
}
96121

97-
- (void)drawRect:(CGRect)rect
122+
- (void)configureLayer
98123
{
99124
if (!_textStorage) {
100125
return;
101126
}
102127

128+
CALayer *currentLayer;
129+
130+
CGSize screenSize = RCTScreenSize();
131+
CGFloat textViewTileSize = MAX(screenSize.width, screenSize.height) * 1.5;
132+
133+
if (self.frame.size.width > textViewTileSize || self.frame.size.height > textViewTileSize) {
134+
// Cleanup sync layer
135+
if (_syncLayer != nil) {
136+
_syncLayer.delegate = nil;
137+
[_syncLayer removeFromSuperlayer];
138+
_syncLayer = nil;
139+
}
140+
141+
if (_asyncTiledLayer == nil) {
142+
RCTTextTiledLayer *layer = [RCTTextTiledLayer layer];
143+
layer.delegate = _renderer;
144+
layer.contentsScale = RCTScreenScale();
145+
layer.tileSize = CGSizeMake(textViewTileSize, textViewTileSize);
146+
_asyncTiledLayer = layer;
147+
[self.layer addSublayer:layer];
148+
[layer setNeedsDisplay];
149+
}
150+
_asyncTiledLayer.frame = self.bounds;
151+
currentLayer = _asyncTiledLayer;
152+
} else {
153+
// Cleanup async tiled layer
154+
if (_asyncTiledLayer != nil) {
155+
_asyncTiledLayer.delegate = nil;
156+
[_asyncTiledLayer removeFromSuperlayer];
157+
_asyncTiledLayer = nil;
158+
}
159+
160+
if (_syncLayer == nil) {
161+
CALayer *layer = [CALayer layer];
162+
layer.delegate = _renderer;
163+
layer.contentsScale = RCTScreenScale();
164+
_syncLayer = layer;
165+
[self.layer addSublayer:layer];
166+
[layer setNeedsDisplay];
167+
}
168+
_syncLayer.frame = self.bounds;
169+
currentLayer = _syncLayer;
170+
}
103171

104172
NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject;
105173
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
106-
107-
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
108-
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:_contentFrame.origin];
109-
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:_contentFrame.origin];
174+
NSRange glyphRange =
175+
[layoutManager glyphRangeForTextContainer:textContainer];
110176

111177
__block UIBezierPath *highlightPath = nil;
112178
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange
113179
actualGlyphRange:NULL];
180+
114181
[_textStorage enumerateAttribute:RCTTextAttributesIsHighlightedAttributeName
115182
inRange:characterRange
116183
options:0
117184
usingBlock:
118-
^(NSNumber *value, NSRange range, __unused BOOL *stop) {
119-
if (!value.boolValue) {
120-
return;
121-
}
122-
123-
[layoutManager enumerateEnclosingRectsForGlyphRange:range
124-
withinSelectedGlyphRange:range
125-
inTextContainer:textContainer
126-
usingBlock:
127-
^(CGRect enclosingRect, __unused BOOL *anotherStop) {
128-
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(enclosingRect, -2, -2) cornerRadius:2];
129-
if (highlightPath) {
130-
[highlightPath appendPath:path];
131-
} else {
132-
highlightPath = path;
133-
}
185+
^(NSNumber *value, NSRange range, __unused BOOL *stop) {
186+
if (!value.boolValue) {
187+
return;
188+
}
189+
190+
[layoutManager enumerateEnclosingRectsForGlyphRange:range
191+
withinSelectedGlyphRange:range
192+
inTextContainer:textContainer
193+
usingBlock:
194+
^(CGRect enclosingRect, __unused BOOL *anotherStop) {
195+
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(enclosingRect, -2, -2) cornerRadius:2];
196+
if (highlightPath) {
197+
[highlightPath appendPath:path];
198+
} else {
199+
highlightPath = path;
134200
}
201+
}
135202
];
136-
}];
203+
}];
137204

138205
if (highlightPath) {
139206
if (!_highlightLayer) {
140207
_highlightLayer = [CAShapeLayer layer];
141208
_highlightLayer.fillColor = [UIColor colorWithWhite:0 alpha:0.25].CGColor;
142-
[self.layer addSublayer:_highlightLayer];
209+
}
210+
if (![currentLayer.sublayers containsObject:_highlightLayer]) {
211+
[currentLayer addSublayer:_highlightLayer];
143212
}
144213
_highlightLayer.position = _contentFrame.origin;
145214
_highlightLayer.path = highlightPath.CGPath;
@@ -149,6 +218,15 @@ - (void)drawRect:(CGRect)rect
149218
}
150219
}
151220

221+
- (void)setCurrentLayerNeedsDisplay
222+
{
223+
if (_asyncTiledLayer != nil) {
224+
[_asyncTiledLayer setNeedsDisplay];
225+
} else if (_syncLayer != nil) {
226+
[_syncLayer setNeedsDisplay];
227+
}
228+
[_highlightLayer setNeedsDisplay];
229+
}
152230

153231
- (NSNumber *)reactTagAtPoint:(CGPoint)point
154232
{
@@ -174,14 +252,18 @@ - (void)didMoveToWindow
174252
{
175253
[super didMoveToWindow];
176254

255+
// When an `RCTText` instance moves offscreen (possibly due to parent clipping),
256+
// we unset the layer's contents until it comes onscreen again.
177257
if (!self.window) {
178-
self.layer.contents = nil;
179-
if (_highlightLayer) {
180-
[_highlightLayer removeFromSuperlayer];
181-
_highlightLayer = nil;
182-
}
258+
[_syncLayer removeFromSuperlayer];
259+
_syncLayer = nil;
260+
[_asyncTiledLayer removeFromSuperlayer];
261+
_asyncTiledLayer = nil;
262+
[_highlightLayer removeFromSuperlayer];
263+
_highlightLayer = nil;
183264
} else if (_textStorage) {
184-
[self setNeedsDisplay];
265+
[self configureLayer];
266+
[self setCurrentLayerNeedsDisplay];
185267
}
186268
}
187269

‎RNTester/js/TextExample.ios.js

+23
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,23 @@ class TextWithCapBaseBox extends React.Component<*, *> {
453453
}
454454
}
455455

456+
function LongTextExample() {
457+
const [collapsed, setCollapsed] = React.useState(true);
458+
return (
459+
<View>
460+
<Button
461+
onPress={() => setCollapsed(state => !state)}
462+
title="Toggle long text"
463+
/>
464+
<Text>
465+
{Array.from({length: collapsed ? 5 : 5000})
466+
.map((_, i) => i)
467+
.join('\n')}
468+
</Text>
469+
</View>
470+
);
471+
}
472+
456473
exports.title = '<Text>';
457474
exports.description = 'Base component for rendering styled text.';
458475
exports.displayName = 'TextExample';
@@ -1125,4 +1142,10 @@ exports.examples = [
11251142
);
11261143
},
11271144
},
1145+
{
1146+
title: 'Async rendering for long text',
1147+
render: function() {
1148+
return <LongTextExample />;
1149+
},
1150+
},
11281151
];

0 commit comments

Comments
 (0)
Please sign in to comment.