Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

Commit 50ca58f

Browse files
committed
Merge pull request #1706 from maicki/FixASTextKitRendererCrashIfNoVisibleRange
[ASTextKitRenderer] Fix crash truncating a string of an ASTextNode with a zero size
2 parents 2e384a3 + 19bb651 commit 50ca58f

8 files changed

Lines changed: 100 additions & 16 deletions

File tree

AsyncDisplayKit/ASTextNode.mm

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point
469469
forHighlighting:(BOOL)highlighting
470470
{
471471
ASTextKitRenderer *renderer = [self _renderer];
472-
NSRange visibleRange = renderer.visibleRanges[0];
472+
NSRange visibleRange = renderer.firstVisibleRange;
473473
NSAttributedString *attributedString = _attributedText;
474474
NSRange clampedRange = NSIntersectionRange(visibleRange, NSMakeRange(0, attributedString.length));
475475

@@ -784,14 +784,18 @@ - (UIImage *)placeholderImage
784784
// FIXME: Replace this implementation with reusable CALayers that have .backgroundColor set.
785785
// This would completely eliminate the memory and performance cost of the backing store.
786786
CGSize size = self.calculatedSize;
787+
if (CGSizeEqualToSize(size, CGSizeZero)) {
788+
return nil;
789+
}
790+
787791
UIGraphicsBeginImageContext(size);
788792
[self.placeholderColor setFill];
789793

790794
ASTextKitRenderer *renderer = [self _renderer];
791-
NSRange textRange = renderer.visibleRanges[0];
795+
NSRange visibleRange = renderer.firstVisibleRange;
792796

793797
// cap height is both faster and creates less subpixel blending
794-
NSArray *lineRects = [self _rectsForTextRange:textRange measureOption:ASTextKitRendererMeasureOptionLineHeight];
798+
NSArray *lineRects = [self _rectsForTextRange:visibleRange measureOption:ASTextKitRendererMeasureOptionLineHeight];
795799

796800
// fill each line with the placeholder color
797801
for (NSValue *rectValue in lineRects) {
@@ -860,7 +864,8 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
860864
BOOL linkCrossesVisibleRange = (lastCharIndex > range.location) && (lastCharIndex < NSMaxRange(range) - 1);
861865

862866
if (inAdditionalTruncationMessage) {
863-
NSRange visibleRange = [self _renderer].visibleRanges[0];
867+
ASTextKitRenderer *renderer = [self _renderer];
868+
NSRange visibleRange = renderer.firstVisibleRange;
864869
NSRange truncationMessageRange = [self _additionalTruncationMessageRangeWithVisibleRange:visibleRange];
865870
[self _setHighlightRange:truncationMessageRange forAttributeName:ASTextNodeTruncationTokenAttributeName value:nil animated:YES];
866871
} else if (range.length && !linkCrossesVisibleRange && linkAttributeValue != nil && linkAttributeName != nil) {
@@ -1054,8 +1059,8 @@ - (void)setTruncationMode:(NSLineBreakMode)truncationMode
10541059

10551060
- (BOOL)isTruncated
10561061
{
1057-
NSRange visibleRange = [self _renderer].visibleRanges[0];
1058-
return visibleRange.length < _attributedText.length;
1062+
ASTextKitRenderer *renderer = [self _renderer];
1063+
return renderer.firstVisibleRange.length < _attributedText.length;
10591064
}
10601065

10611066
- (void)setPointSizeScaleFactors:(NSArray *)pointSizeScaleFactors

AsyncDisplayKit/TextKit/ASTextKitRenderer+TextChecking.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ - (NSTextCheckingResult *)textCheckingResultAtPoint:(CGPoint)point
5959
NSAttributedString *truncationAttributedString = self.attributes.truncationAttributedString;
6060

6161
// get the index of the last character, so we can handle text in the truncation token
62-
NSRange visibleRange = self.truncater.visibleRanges[0];
6362
__block NSRange truncationTokenRange = { NSNotFound, 0 };
6463

6564
[truncationAttributedString enumerateAttribute:ASTextKitTruncationAttributeName inRange:NSMakeRange(0, truncationAttributedString.length)
@@ -75,6 +74,7 @@ - (NSTextCheckingResult *)textCheckingResultAtPoint:(CGPoint)point
7574
truncationTokenRange = { 0, truncationAttributedString.length };
7675
}
7776

77+
NSRange visibleRange = self.truncater.firstVisibleRange;
7878
truncationTokenRange.location += NSMaxRange(visibleRange);
7979

8080
__block CGFloat minDistance = CGFLOAT_MAX;

AsyncDisplayKit/TextKit/ASTextKitRenderer.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,21 @@
7878
The character range from the original attributedString that is displayed by the renderer given the parameters in the
7979
initializer.
8080
*/
81-
- (std::vector<NSRange>)visibleRanges;
81+
@property (nonatomic, assign, readonly) std::vector<NSRange> visibleRanges;
8282

8383
/**
8484
The number of lines shown in the string.
8585
*/
8686
- (NSUInteger)lineCount;
8787

8888
@end
89+
90+
@interface ASTextKitRenderer (ASTextKitRendererConvenience)
91+
92+
/**
93+
Returns the first visible range or an NSRange with location of NSNotFound and size of 0 if no first visible
94+
range exists
95+
*/
96+
@property (nonatomic, assign, readonly) NSRange firstVisibleRange;
97+
98+
@end

AsyncDisplayKit/TextKit/ASTextKitRenderer.mm

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,17 @@ - (NSUInteger)lineCount
262262
}
263263

264264
@end
265+
266+
@implementation ASTextKitRenderer (ASTextKitRendererConvenience)
267+
268+
- (NSRange)firstVisibleRange
269+
{
270+
std::vector<NSRange> visibleRanges = self.visibleRanges;
271+
if (visibleRanges.size() > 0) {
272+
return visibleRanges[0];
273+
}
274+
275+
return NSMakeRange(NSNotFound, 0);
276+
}
277+
278+
@end

AsyncDisplayKit/TextKit/ASTextKitTailTruncater.mm

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ @implementation ASTextKitTailTruncater
2020
NSCharacterSet *_avoidTailTruncationSet;
2121
}
2222
@synthesize visibleRanges = _visibleRanges;
23-
@synthesize truncationStringRect = _truncationStringRect;
2423

2524
- (instancetype)initWithContext:(ASTextKitContext *)context
2625
truncationAttributedString:(NSAttributedString *)truncationAttributedString
@@ -185,4 +184,14 @@ - (void)truncate
185184
}];
186185
}
187186

187+
- (NSRange)firstVisibleRange
188+
{
189+
std::vector<NSRange> visibleRanges = _visibleRanges;
190+
if (visibleRanges.size() > 0) {
191+
return visibleRanges[0];
192+
}
193+
194+
return NSMakeRange(NSNotFound, 0);
195+
}
196+
188197
@end

AsyncDisplayKit/TextKit/ASTextKitTruncating.h

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,21 @@
1414

1515
#import "ASTextKitRenderer.h"
1616

17+
NS_ASSUME_NONNULL_BEGIN
18+
1719
@protocol ASTextKitTruncating <NSObject>
1820

21+
/**
22+
The character range from the original attributedString that is displayed by the renderer given the parameters in the
23+
initializer.
24+
*/
1925
@property (nonatomic, assign, readonly) std::vector<NSRange> visibleRanges;
20-
@property (nonatomic, assign, readonly) CGRect truncationStringRect;
26+
27+
/**
28+
Returns the first visible range or an NSRange with location of NSNotFound and size of 0 if no first visible
29+
range exists
30+
*/
31+
@property (nonatomic, assign, readonly) NSRange firstVisibleRange;
2132

2233
/**
2334
A truncater object is initialized with the full state of the text. It is a Single Responsibility Object that is
@@ -30,12 +41,14 @@
3041
The truncater should not store a strong reference to the context to prevent retain cycles.
3142
*/
3243
- (instancetype)initWithContext:(ASTextKitContext *)context
33-
truncationAttributedString:(NSAttributedString *)truncationAttributedString
34-
avoidTailTruncationSet:(NSCharacterSet *)avoidTailTruncationSet;
44+
truncationAttributedString:(NSAttributedString * _Nullable)truncationAttributedString
45+
avoidTailTruncationSet:(NSCharacterSet * _Nullable)avoidTailTruncationSet;
3546

3647
/**
37-
* Actually do the truncation.
48+
Actually do the truncation.
3849
*/
3950
- (void)truncate;
4051

4152
@end
53+
54+
NS_ASSUME_NONNULL_END

AsyncDisplayKitTests/ASTextKitTruncationTests.mm

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ - (void)testEmptyTruncationStringSameAsStraightTextKitTailTruncation
5555
avoidTailTruncationSet:nil];
5656
[tailTruncater truncate];
5757
XCTAssert(NSEqualRanges(textKitVisibleRange, tailTruncater.visibleRanges[0]));
58+
XCTAssert(NSEqualRanges(textKitVisibleRange, tailTruncater.firstVisibleRange));
5859
}
5960

6061
- (void)testSimpleTailTruncation
@@ -80,6 +81,7 @@ - (void)testSimpleTailTruncation
8081
NSString *expectedString = @"90's cray photo booth tote bag bespoke Carles. Plaid wayfarers...";
8182
XCTAssertEqualObjects(expectedString, drawnString);
8283
XCTAssert(NSEqualRanges(NSMakeRange(0, 62), tailTruncater.visibleRanges[0]));
84+
XCTAssert(NSEqualRanges(NSMakeRange(0, 62), tailTruncater.firstVisibleRange));
8385
}
8486

8587
- (void)testAvoidedCharTailWordBoundaryTruncation
@@ -132,6 +134,27 @@ - (void)testAvoidedCharTailCharBoundaryTruncation
132134
XCTAssertEqualObjects(expectedString, drawnString);
133135
}
134136

137+
- (void)testHandleZeroSizeConstrainedSize
138+
{
139+
CGSize constrainedSize = CGSizeZero;
140+
NSAttributedString *attributedString = [self _sentenceAttributedString];
141+
142+
ASTextKitContext *context = [[ASTextKitContext alloc] initWithAttributedString:attributedString
143+
lineBreakMode:NSLineBreakByWordWrapping
144+
maximumNumberOfLines:0
145+
exclusionPaths:nil
146+
constrainedSize:constrainedSize
147+
layoutManagerCreationBlock:nil
148+
layoutManagerDelegate:nil
149+
textStorageCreationBlock:nil];
150+
ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context
151+
truncationAttributedString:[self _simpleTruncationAttributedString]
152+
avoidTailTruncationSet:nil];
153+
XCTAssertNoThrow([tailTruncater truncate]);
154+
XCTAssert(tailTruncater.visibleRanges.size() == 0);
155+
NSEqualRanges(NSMakeRange(NSNotFound, 0), tailTruncater.firstVisibleRange);
156+
}
157+
135158
- (void)testHandleZeroHeightConstrainedSize
136159
{
137160
CGSize constrainedSize = CGSizeMake(50, 0);
@@ -145,9 +168,10 @@ - (void)testHandleZeroHeightConstrainedSize
145168
layoutManagerDelegate:nil
146169
textStorageCreationBlock:nil];
147170

148-
XCTAssertNoThrow([[[ASTextKitTailTruncater alloc] initWithContext:context
149-
truncationAttributedString:[self _simpleTruncationAttributedString]
150-
avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]] truncate]);
171+
ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context
172+
truncationAttributedString:[self _simpleTruncationAttributedString]
173+
avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]];
174+
XCTAssertNoThrow([tailTruncater truncate]);
151175
}
152176

153177
@end

AsyncDisplayKitTests/ASTextNodeTests.m

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ - (void)testRecalculationOfSizeIsSameAsOriginallyCalculatedFloatingPointSize
140140
}
141141
}
142142

143+
- (void)testMeasureWithZeroSizeAndPlaceholder
144+
{
145+
_textNode.placeholderEnabled = YES;
146+
147+
XCTAssertNoThrow([_textNode measure:CGSizeZero], @"Measure with zero size and placeholder enabled should not throw an exception");
148+
XCTAssertNoThrow([_textNode measure:CGSizeMake(0, 100)], @"Measure with zero width and placeholder enabled should not throw an exception");
149+
XCTAssertNoThrow([_textNode measure:CGSizeMake(100, 0)], @"Measure with zero height and placeholder enabled should not throw an exception");
150+
}
151+
143152
- (void)testAccessibility
144153
{
145154
_textNode.attributedText = _attributedText;

0 commit comments

Comments
 (0)