From beb98b448ec6f325d2f30c83851c8a2bbb5f131a Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Sun, 20 Nov 2016 21:13:37 -0800 Subject: [PATCH] [ASTextKit] Remove internal side effects related to constrainedSize. --- AsyncDisplayKit/ASTextNode.mm | 254 ++++++------------ AsyncDisplayKit/TextKit/ASTextKitAttributes.h | 3 +- AsyncDisplayKit/TextKit/ASTextKitContext.h | 2 - AsyncDisplayKit/TextKit/ASTextKitContext.mm | 12 - AsyncDisplayKit/TextKit/ASTextKitRenderer.mm | 77 ++---- .../ASTextNodeSnapshotTests.m | 7 + AsyncDisplayKitTests/ASTextNodeTests.m | 4 +- ...sIncludedWithSmallerConstrainedSize@2x.png | Bin 5993 -> 5918 bytes 8 files changed, 119 insertions(+), 240 deletions(-) diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 9eb503a13e..5539a95116 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -50,6 +50,77 @@ struct ASTextNodeDrawParameter { UIColor *backgroundColor; }; +#pragma mark - ASTextKitRenderer + +// Not used at the moment but handy to have +/*ASDISPLAYNODE_INLINE NSUInteger ASHashFromCGRect(CGRect rect) +{ + return ((*(NSUInteger *)&rect.origin.x << 10 ^ *(NSUInteger *)&rect.origin.y) + (*(NSUInteger *)&rect.size.width << 10 ^ *(NSUInteger *)&rect.size.height)); +}*/ + +ASDISPLAYNODE_INLINE NSUInteger ASHashFromCGSize(CGSize size) +{ + return ((*(NSUInteger *)&size.width << 10 ^ *(NSUInteger *)&size.height)); +} + +@interface ASTextNodeRendererKey : NSObject +@property (assign, nonatomic) ASTextKitAttributes attributes; +@property (assign, nonatomic) CGSize constrainedSize; +@end + +@implementation ASTextNodeRendererKey + +- (NSUInteger)hash +{ + return _attributes.hash() ^ ASHashFromCGSize(_constrainedSize); +} + +- (BOOL)isEqual:(ASTextNodeRendererKey *)object +{ + if (self == object) { + return YES; + } + + return _attributes.hash() == object.attributes.hash() + && CGSizeEqualToSize(_constrainedSize, object.constrainedSize); +} + +@end + +static NSCache *sharedRendererCache() +{ + static dispatch_once_t onceToken; + static NSCache *__rendererCache = nil; + dispatch_once(&onceToken, ^{ + __rendererCache = [[NSCache alloc] init]; + __rendererCache.countLimit = 500; // 500 renders cache + }); + return __rendererCache; +} + +/** + The concept here is that neither the node nor layout should ever have a strong reference to the renderer object. + This is to reduce memory load when loading thousands and thousands of text nodes into memory at once. Instead + we maintain a LRU renderer cache that is queried via a unique key based on text kit attributes and constrained size. + */ + +static ASTextKitRenderer *rendererForAttributes(ASTextKitAttributes attributes, CGSize constrainedSize) +{ + NSCache *cache = sharedRendererCache(); + + ASTextNodeRendererKey *key = [[ASTextNodeRendererKey alloc] init]; + key.attributes = attributes; + key.constrainedSize = constrainedSize; + + ASTextKitRenderer *renderer = [cache objectForKey:key]; + if (renderer == nil) { + renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:attributes constrainedSize:constrainedSize]; + [cache setObject:renderer forKey:key]; + } + + return renderer; +} + @interface ASTextNode () @end @@ -73,10 +144,6 @@ struct ASTextNodeDrawParameter { NSRange _highlightRange; ASHighlightOverlayLayer *_activeHighlightLayer; - CGSize _constrainedSize; - - ASTextKitRenderer *_renderer; - ASTextNodeDrawParameter _drawParameter; UILongPressGestureRecognizer *_longPressGestureRecognizer; @@ -123,8 +190,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; self.isAccessibilityElement = YES; self.accessibilityTraits = UIAccessibilityTraitStaticText; - _constrainedSize = CGSizeMake(-INFINITY, -INFINITY); - // Placeholders // Disabled by default in ASDisplayNode, but add a few options for those who toggle // on the special placeholder behavior of ASTextNode. @@ -139,8 +204,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; { CGColorRelease(_shadowColor); - [self _invalidateRenderer]; - if (_longPressGestureRecognizer) { _longPressGestureRecognizer.delegate = nil; [_longPressGestureRecognizer removeTarget:nil action:NULL]; @@ -181,27 +244,12 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - ASDisplayNode -// FIXME: Re-evaluate if it is still the right decision to clear the renderer at this stage. -// This code was written before TextKit and when 512MB devices were still the overwhelming majority. -- (void)displayDidFinish -{ - [super displayDidFinish]; - - // We invalidate our renderer here to clear the very high memory cost of - // keeping this around. _invalidateRenderer will dealloc this onto a bg - // thread resulting in less stutters on the main thread than if it were - // to be deallocated in dealloc. This is also helpful in opportunistically - // reducing memory consumption and reducing the overall footprint of the app. - [self _invalidateRenderer]; -} - - (void)clearContents { // We discard the backing store and renderer to prevent the very large // memory overhead of maintaining these for all text nodes. They can be // regenerated when layout is necessary. [super clearContents]; // ASDisplayNode will set layer.contents = nil - [self _invalidateRenderer]; } - (void)didLoad @@ -218,45 +266,23 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } } -- (void)setFrame:(CGRect)frame -{ - [super setFrame:frame]; - [self _invalidateRendererIfNeededForBoundsSize:frame.size]; -} - -- (void)setBounds:(CGRect)bounds -{ - [super setBounds:bounds]; - [self _invalidateRendererIfNeededForBoundsSize:bounds.size]; -} - #pragma mark - Renderer Management - (ASTextKitRenderer *)_renderer { - return [self _rendererWithBounds:self.threadSafeBounds]; + CGSize constrainedSize = self.threadSafeBounds.size; + return [self _rendererWithBoundsSlow:{.size = constrainedSize}]; } -- (ASTextKitRenderer *)_rendererWithBounds:(CGRect)bounds +- (ASTextKitRenderer *)_rendererWithBoundsSlow:(CGRect)bounds { ASDN::MutexLocker l(__instanceLock__); - - if (_renderer == nil) { - CGSize constrainedSize; - if (_constrainedSize.width != -INFINITY) { - constrainedSize = _constrainedSize; - } else { - constrainedSize = bounds.size; - constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right); - constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom); - } - - _renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:[self _rendererAttributes] - constrainedSize:constrainedSize]; - } - return _renderer; + bounds.size.width -= (_textContainerInset.left + _textContainerInset.right); + bounds.size.height -= (_textContainerInset.top + _textContainerInset.bottom); + return rendererForAttributes([self _rendererAttributes], bounds.size); } + - (ASTextKitAttributes)_rendererAttributes { ASDN::MutexLocker l(__instanceLock__); @@ -276,38 +302,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; }; } -- (void)_invalidateRendererIfNeeded -{ - [self _invalidateRendererIfNeededForBoundsSize:self.threadSafeBounds.size]; -} - -- (void)_invalidateRendererIfNeededForBoundsSize:(CGSize)boundsSize -{ - if ([self _needInvalidateRendererForBoundsSize:boundsSize]) { - // Our bounds have changed to a size that is not identical to our constraining size, - // so our previous layout information is invalid, and TextKit may draw at the - // incorrect origin. - { - ASDN::MutexLocker l(__instanceLock__); - _constrainedSize = CGSizeMake(-INFINITY, -INFINITY); - } - [self _invalidateRenderer]; - } -} - -- (void)_invalidateRenderer -{ - ASDN::MutexLocker l(__instanceLock__); - - if (_renderer) { - // Destruction of the layout managers/containers/text storage is quite - // expensive, and can take some time, so we dispatch onto a bg queue to - // actually dealloc. - ASPerformBackgroundDeallocation(_renderer); - _renderer = nil; - } -} - #pragma mark - Layout and Sizing - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset @@ -327,60 +321,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; return _textContainerInset; } -- (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize -{ - ASDN::MutexLocker l(__instanceLock__); - - if (_renderer == nil) { - return YES; - } - - // If the size is not the same as the constraint we provided to the renderer, start out assuming we need - // a new one. However, there are common cases where the constrained size doesn't need to be the same as calculated. - CGSize rendererConstrainedSize = _renderer.constrainedSize; - - //inset bounds - boundsSize.width -= _textContainerInset.left + _textContainerInset.right; - boundsSize.height -= _textContainerInset.top + _textContainerInset.bottom; - - if (CGSizeEqualToSize(boundsSize, rendererConstrainedSize)) { - return NO; - } else { - // It is very common to have a constrainedSize with a concrete, specific width but +Inf height. - // In this case, as long as the text node has bounds as large as the full calculatedLayout suggests, - // it means that the text has all the room it needs (as it was not vertically bounded). So, we will not - // experience truncation and don't need to recreate the renderer with the size it already calculated, - // as this would essentially serve to set its constrainedSize to be its calculatedSize (unnecessary). - ASLayout *layout = self.calculatedLayout; - if (layout != nil && CGSizeEqualToSize(boundsSize, layout.size)) { - return (boundsSize.width != rendererConstrainedSize.width); - } else { - return YES; - } - } -} - -- (void)calculatedLayoutDidChange -{ - [super calculatedLayoutDidChange]; - - ASLayout *layout = self.calculatedLayout; - - if (layout != nil) { - ASDN::MutexLocker l(__instanceLock__); - CGSize layoutSize = layout.size; - - // Apply textContainerInset - layoutSize.width -= (_textContainerInset.left + _textContainerInset.right); - layoutSize.height -= (_textContainerInset.top + _textContainerInset.bottom); - - if (CGSizeEqualToSize(_constrainedSize, layoutSize) == NO) { - _constrainedSize = layoutSize; - [self _invalidateRenderer]; - } - } -} - - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { ASDN::MutexLocker l(__instanceLock__); @@ -390,27 +330,18 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // Cache the original constrained size for final size calculateion CGSize originalConstrainedSize = constrainedSize; - - // Adjust constrainedSize for textContainerInset before assigning it - constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right); - constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom); - - _constrainedSize = constrainedSize; - - if (_renderer != nil && CGSizeEqualToSize(constrainedSize, _renderer.constrainedSize) == NO) { - [self _invalidateRenderer]; - } [self setNeedsDisplay]; - CGSize size = [self _renderer].size; + ASTextKitRenderer *renderer = [self _rendererWithBoundsSlow:{.size = constrainedSize}]; + CGSize size = renderer.size; if (_attributedText.length > 0) { self.style.ascender = [[self class] ascenderWithAttributedString:_attributedText]; self.style.descender = [[_attributedText attribute:NSFontAttributeName atIndex:_attributedText.length - 1 effectiveRange:NULL] descender]; - if (_renderer.currentScaleFactor > 0 && _renderer.currentScaleFactor < 1.0) { + if (renderer.currentScaleFactor > 0 && renderer.currentScaleFactor < 1.0) { // while not perfect, this is a good estimate of what the ascender of the scaled font will be. - self.style.ascender *= _renderer.currentScaleFactor; - self.style.descender *= _renderer.currentScaleFactor; + self.style.ascender *= renderer.currentScaleFactor; + self.style.descender *= renderer.currentScaleFactor; } } @@ -461,9 +392,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // Without this, the size calculation of the text with truncation applied will // not take into account the attributes of attributedText in the last line [self _updateComposedTruncationText]; - - // We need an entirely new renderer - [self _invalidateRenderer]; } NSUInteger length = attributedText.length; @@ -495,7 +423,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } _exclusionPaths = [exclusionPaths copy]; - [self _invalidateRenderer]; [self setNeedsLayout]; [self setNeedsDisplay]; } @@ -536,7 +463,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; CGContextTranslateCTM(context, _textContainerInset.left, _textContainerInset.top); - ASTextKitRenderer *renderer = [self _rendererWithBounds:drawParameterBounds]; + ASTextKitRenderer *renderer = [self _rendererWithBoundsSlow:drawParameterBounds]; // Fill background if (backgroundColor != nil) { @@ -790,11 +717,12 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; if (highlightTargetLayer != nil) { ASDN::MutexLocker l(__instanceLock__); + ASTextKitRenderer *renderer = [self _renderer]; - NSArray *highlightRects = [[self _renderer] rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock]; + NSArray *highlightRects = [renderer rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock]; NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count]; for (NSValue *rectValue in highlightRects) { - UIEdgeInsets shadowPadding = _renderer.shadower.shadowPadding; + UIEdgeInsets shadowPadding = renderer.shadower.shadowPadding; CGRect rendererRect = ASTextNodeAdjustRenderRectForShadowPadding(rectValue.CGRectValue, shadowPadding); // The rects returned from renderer don't have `textContainerInset`, @@ -1119,7 +1047,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI CGColorRelease(_shadowColor); _shadowColor = CGColorRetain(shadowColor); _cachedShadowUIColor = [UIColor colorWithCGColor:shadowColor]; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1137,7 +1064,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI if (!CGSizeEqualToSize(_shadowOffset, shadowOffset)) { _shadowOffset = shadowOffset; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1155,7 +1081,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI if (_shadowOpacity != shadowOpacity) { _shadowOpacity = shadowOpacity; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1173,7 +1098,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI if (_shadowRadius != shadowRadius) { _shadowRadius = shadowRadius; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1232,7 +1156,6 @@ static NSAttributedString *DefaultTruncationAttributedString() if (_truncationMode != truncationMode) { _truncationMode = truncationMode; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1251,7 +1174,6 @@ static NSAttributedString *DefaultTruncationAttributedString() if ([_pointSizeScaleFactors isEqualToArray:pointSizeScaleFactors] == NO) { _pointSizeScaleFactors = pointSizeScaleFactors; - [self _invalidateRenderer]; [self setNeedsDisplay]; }} @@ -1261,7 +1183,6 @@ static NSAttributedString *DefaultTruncationAttributedString() if (_maximumNumberOfLines != maximumNumberOfLines) { _maximumNumberOfLines = maximumNumberOfLines; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1285,7 +1206,6 @@ static NSAttributedString *DefaultTruncationAttributedString() - (void)_invalidateTruncationText { [self _updateComposedTruncationText]; - [self _invalidateRenderer]; [self setNeedsDisplay]; } diff --git a/AsyncDisplayKit/TextKit/ASTextKitAttributes.h b/AsyncDisplayKit/TextKit/ASTextKitAttributes.h index 0021ec94cb..5b77fcfc52 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitAttributes.h +++ b/AsyncDisplayKit/TextKit/ASTextKitAttributes.h @@ -110,7 +110,8 @@ struct ASTextKitAttributes { && maximumNumberOfLines == other.maximumNumberOfLines && shadowOpacity == other.shadowOpacity && shadowRadius == other.shadowRadius - && [pointSizeScaleFactors isEqualToArray:other.pointSizeScaleFactors] + && (pointSizeScaleFactors == other.pointSizeScaleFactors + || [pointSizeScaleFactors isEqualToArray:other.pointSizeScaleFactors]) && CGSizeEqualToSize(shadowOffset, other.shadowOffset) && ASObjectIsEqual(exclusionPaths, other.exclusionPaths) && ASObjectIsEqual(avoidTailTruncationSet, other.avoidTailTruncationSet) diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.h b/AsyncDisplayKit/TextKit/ASTextKitContext.h index 58257efbab..9e90fac710 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitContext.h +++ b/AsyncDisplayKit/TextKit/ASTextKitContext.h @@ -29,8 +29,6 @@ exclusionPaths:(NSArray *)exclusionPaths constrainedSize:(CGSize)constrainedSize; -@property (nonatomic, assign, readwrite) CGSize constrainedSize; - /** All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to TextKit components may cause crashes. diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.mm b/AsyncDisplayKit/TextKit/ASTextKitContext.mm index ba7477c500..7eca01d531 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitContext.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitContext.mm @@ -54,18 +54,6 @@ return self; } -- (CGSize)constrainedSize -{ - ASDN::MutexSharedLocker l(__instanceLock__); - return _textContainer.size; -} - -- (void)setConstrainedSize:(CGSize)constrainedSize -{ - ASDN::MutexSharedLocker l(__instanceLock__); - _textContainer.size = constrainedSize; -} - - (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *, NSTextStorage *, NSTextContainer *))block diff --git a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm index d4246b2a1c..1967b98264 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm @@ -37,7 +37,6 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() @implementation ASTextKitRenderer { CGSize _calculatedSize; - BOOL _sizeIsCalculated; } @synthesize attributes = _attributes, context = _context, shadower = _shadower, truncater = _truncater, fontSizeAdjuster = _fontSizeAdjuster; @@ -49,62 +48,38 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() if (self = [super init]) { _constrainedSize = constrainedSize; _attributes = attributes; - _sizeIsCalculated = NO; _currentScaleFactor = 1; - } - return self; -} - -- (ASTextKitShadower *)shadower -{ - if (!_shadower) { - ASTextKitAttributes attributes = _attributes; + + // As the renderer should be thread safe, create all subcomponents in the initialization method _shadower = [ASTextKitShadower shadowerWithShadowOffset:attributes.shadowOffset shadowColor:attributes.shadowColor shadowOpacity:attributes.shadowOpacity shadowRadius:attributes.shadowRadius]; - } - return _shadower; -} - -- (ASTextKitTailTruncater *)truncater -{ - if (!_truncater) { - ASTextKitAttributes attributes = _attributes; - NSCharacterSet *avoidTailTruncationSet = attributes.avoidTailTruncationSet ? : _defaultAvoidTruncationCharacterSet(); - _truncater = [[ASTextKitTailTruncater alloc] initWithContext:[self context] - truncationAttributedString:attributes.truncationAttributedString - avoidTailTruncationSet:avoidTailTruncationSet]; - } - return _truncater; -} - -- (ASTextKitFontSizeAdjuster *)fontSizeAdjuster -{ - if (!_fontSizeAdjuster) { - ASTextKitAttributes attributes = _attributes; - // We must inset the constrained size by the size of the shadower. - CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize]; - _fontSizeAdjuster = [[ASTextKitFontSizeAdjuster alloc] initWithContext:[self context] - constrainedSize:shadowConstrainedSize - textKitAttributes:attributes]; - } - return _fontSizeAdjuster; -} - -- (ASTextKitContext *)context -{ - if (!_context) { - ASTextKitAttributes attributes = _attributes; + // We must inset the constrained size by the size of the shadower. CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize]; + _context = [[ASTextKitContext alloc] initWithAttributedString:attributes.attributedString lineBreakMode:attributes.lineBreakMode maximumNumberOfLines:attributes.maximumNumberOfLines exclusionPaths:attributes.exclusionPaths constrainedSize:shadowConstrainedSize]; + + NSCharacterSet *avoidTailTruncationSet = attributes.avoidTailTruncationSet ?: _defaultAvoidTruncationCharacterSet(); + _truncater = [[ASTextKitTailTruncater alloc] initWithContext:[self context] + truncationAttributedString:attributes.truncationAttributedString + avoidTailTruncationSet:avoidTailTruncationSet]; + + ASTextKitAttributes attributes = _attributes; + // We must inset the constrained size by the size of the shadower. + _fontSizeAdjuster = [[ASTextKitFontSizeAdjuster alloc] initWithContext:[self context] + constrainedSize:shadowConstrainedSize + textKitAttributes:attributes]; + + // Calcualate size immediately + [self _calculateSize]; } - return _context; + return self; } - (NSStringDrawingContext *)stringDrawingContext @@ -127,10 +102,6 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() - (CGSize)size { - if (!_sizeIsCalculated) { - [self _calculateSize]; - _sizeIsCalculated = YES; - } return _calculatedSize; } @@ -222,12 +193,6 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() { // We add an assertion so we can track the rare conditions where a graphics context is not present ASDisplayNodeAssertNotNil(context, @"This is no good without a context."); - - // This renderer may not be the one that did the sizing. If that is the case its truncation and currentScaleFactor may not have been evaluated. - // If there's any possibility we need to truncate or scale (i.e. width is not infinite), perform the size calculation. - if (_sizeIsCalculated == NO && isinf(_constrainedSize.width) == NO) { - [self _calculateSize]; - } bounds = CGRectIntersection(bounds, { .size = _constrainedSize }); CGRect shadowInsetBounds = [[self shadower] insetRectWithConstrainedRect:bounds]; @@ -298,9 +263,7 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() - (std::vector)visibleRanges { - ASTextKitTailTruncater *truncater = [self truncater]; - [truncater truncate]; - return truncater.visibleRanges; + return _truncater.visibleRanges; } @end diff --git a/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m index 2a2a1b704d..c0acf7ad4c 100644 --- a/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m +++ b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m @@ -18,6 +18,13 @@ @implementation ASTextNodeSnapshotTests +- (void)setUp +{ + [super setUp]; + + self.recordMode = NO; +} + - (void)testTextContainerInset { // trivial test case to ensure ASSnapshotTestCase works diff --git a/AsyncDisplayKitTests/ASTextNodeTests.m b/AsyncDisplayKitTests/ASTextNodeTests.m index ff1620cd72..7395e15b3b 100644 --- a/AsyncDisplayKitTests/ASTextNodeTests.m +++ b/AsyncDisplayKitTests/ASTextNodeTests.m @@ -171,7 +171,9 @@ ASTextNodeTestDelegate *delegate = [ASTextNodeTestDelegate new]; _textNode.delegate = delegate; - [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))]; + ASLayout *layout = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))]; + _textNode.frame = CGRectMake(0, 0, layout.size.width, layout.size.height); + NSRange returnedLinkRange; NSString *returnedAttributeName; NSString *returnedLinkAttributeValue = [_textNode linkAttributeValueAtPoint:CGPointMake(3, 3) attributeName:&returnedAttributeName range:&returnedLinkRange]; diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png index 4f8364937f04fb60b0b90dc97e5abdb8386f4dfd..65c801d4dffbb0a823db8d1e9387adddb7d12a5f 100644 GIT binary patch literal 5918 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 XmgyTwS%kDOlR0|W+qL3Q@Qr@~>>R$a literal 5993 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