[ASTextNode] Fix text node truncation (#1863)

* Before truncate a text storage in ASTextKitContext reset the text storage to original value

* Fix ASTextNode tests

We should pass in the constrained size in both cases and the sizes should be the same. We adjust the calculated size in ASTextNode to be a bit narrower in the second case if we truncate again with the calculated size as constrained size it will truncate more and the resulting size will shrink.
This commit is contained in:
Michael Schneider
2016-07-09 15:40:31 -07:00
committed by appleguy
parent 38fab7cd94
commit 6238e5edbd
5 changed files with 56 additions and 8 deletions

View File

@@ -10,6 +10,8 @@
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
typedef NSTextStorage *(^ASTextKitContextTextStorageCreationBlock)(NSAttributedString *attributedString);
/** /**
A threadsafe container for the TextKit components that ASTextKit uses to lay out and truncate its text. A threadsafe container for the TextKit components that ASTextKit uses to lay out and truncate its text.
@@ -30,10 +32,19 @@
constrainedSize:(CGSize)constrainedSize constrainedSize:(CGSize)constrainedSize
layoutManagerCreationBlock:(NSLayoutManager * (^)(void))layoutCreationBlock layoutManagerCreationBlock:(NSLayoutManager * (^)(void))layoutCreationBlock
layoutManagerDelegate:(id<NSLayoutManagerDelegate>)layoutManagerDelegate layoutManagerDelegate:(id<NSLayoutManagerDelegate>)layoutManagerDelegate
textStorageCreationBlock:(NSTextStorage * (^)(NSAttributedString *attributedString))textStorageCreationBlock; textStorageCreationBlock:(ASTextKitContextTextStorageCreationBlock)textStorageCreationBlock;
/**
Set the constrained size for the text context.
*/
@property (nonatomic, assign, readwrite) CGSize constrainedSize; @property (nonatomic, assign, readwrite) CGSize constrainedSize;
/**
Resets the text storage to the original value in case it was truncated before. This method is called within
a locked context.
*/
- (void)resetTextStorage;
/** /**
All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to
TextKit components may cause crashes. TextKit components may cause crashes.

View File

@@ -21,8 +21,12 @@
NSLayoutManager *_layoutManager; NSLayoutManager *_layoutManager;
NSTextStorage *_textStorage; NSTextStorage *_textStorage;
NSTextContainer *_textContainer; NSTextContainer *_textContainer;
NSAttributedString *_attributedString;
} }
#pragma mark - Lifecycle
- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString - (instancetype)initWithAttributedString:(NSAttributedString *)attributedString
lineBreakMode:(NSLineBreakMode)lineBreakMode lineBreakMode:(NSLineBreakMode)lineBreakMode
maximumNumberOfLines:(NSUInteger)maximumNumberOfLines maximumNumberOfLines:(NSUInteger)maximumNumberOfLines
@@ -37,16 +41,23 @@
// Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock. // Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock.
static std::mutex __static_mutex; static std::mutex __static_mutex;
std::lock_guard<std::mutex> l(__static_mutex); std::lock_guard<std::mutex> l(__static_mutex);
_attributedString = [attributedString copy];
// Create the TextKit component stack with our default configuration. // Create the TextKit component stack with our default configuration.
if (textStorageCreationBlock) { if (textStorageCreationBlock) {
_textStorage = textStorageCreationBlock(attributedString); _textStorage = textStorageCreationBlock(attributedString);
} else { } else {
_textStorage = (attributedString ? [[NSTextStorage alloc] initWithAttributedString:attributedString] : [[NSTextStorage alloc] init]); _textStorage = [[NSTextStorage alloc] init];
[self _resetTextStorage];
} }
_layoutManager = layoutCreationBlock ? layoutCreationBlock() : [[ASLayoutManager alloc] init]; _layoutManager = layoutCreationBlock ? layoutCreationBlock() : [[ASLayoutManager alloc] init];
_layoutManager.usesFontLeading = NO; _layoutManager.usesFontLeading = NO;
_layoutManager.delegate = layoutManagerDelegate; _layoutManager.delegate = layoutManagerDelegate;
[_textStorage addLayoutManager:_layoutManager]; [_textStorage addLayoutManager:_layoutManager];
_textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize]; _textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize];
// We want the text laid out up to the very edges of the container. // We want the text laid out up to the very edges of the container.
_textContainer.lineFragmentPadding = 0; _textContainer.lineFragmentPadding = 0;
@@ -58,6 +69,21 @@
return self; return self;
} }
#pragma mark - Text Storage
- (void)resetTextStorage
{
std::lock_guard<std::mutex> l(_textKitMutex);
[self _resetTextStorage];
}
- (void)_resetTextStorage
{
[_textStorage setAttributedString:_attributedString];
}
#pragma mark - Setter / Getter
- (CGSize)constrainedSize - (CGSize)constrainedSize
{ {
std::lock_guard<std::mutex> l(_textKitMutex); std::lock_guard<std::mutex> l(_textKitMutex);
@@ -70,6 +96,8 @@
_textContainer.size = constrainedSize; _textContainer.size = constrainedSize;
} }
#pragma mark - Locking
- (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *, - (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *,
NSTextStorage *, NSTextStorage *,
NSTextContainer *))block NSTextContainer *))block

View File

@@ -129,8 +129,12 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet()
// If we're updating an existing context, make sure to use the same inset logic used during initialization. // If we're updating an existing context, make sure to use the same inset logic used during initialization.
// This codepath allows us to reuse the // This codepath allows us to reuse the
CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:constrainedSize]; CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:constrainedSize];
if (_context) _context.constrainedSize = shadowConstrainedSize; if (_context) {
if (_fontSizeAdjuster) _fontSizeAdjuster.constrainedSize = shadowConstrainedSize; _context.constrainedSize = shadowConstrainedSize;
}
if (_fontSizeAdjuster) {
_fontSizeAdjuster.constrainedSize = shadowConstrainedSize;
}
} }
} }
} }

View File

@@ -152,9 +152,14 @@
- (void)truncate - (void)truncate
{ {
[_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { // Reset the text storage to start always with the full string
NSUInteger originalStringLength = textStorage.length; [_context resetTextStorage];
// Start truncation
[_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
NSUInteger originalStringLength = textStorage.length;
[layoutManager ensureLayoutForTextContainer:textContainer]; [layoutManager ensureLayoutForTextContainer:textContainer];
NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect:{ .size = textContainer.size } NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect:{ .size = textContainer.size }

View File

@@ -125,7 +125,7 @@ static BOOL CGSizeEqualToSizeWithIn(CGSize size1, CGSize size2, CGFloat delta)
for (NSInteger i = 10; i < 500; i += 50) { for (NSInteger i = 10; i < 500; i += 50) {
CGSize constrainedSize = CGSizeMake(i, i); CGSize constrainedSize = CGSizeMake(i, i);
CGSize calculatedSize = [_textNode measure:constrainedSize]; CGSize calculatedSize = [_textNode measure:constrainedSize];
CGSize recalculatedSize = [_textNode measure:calculatedSize]; CGSize recalculatedSize = [_textNode measure:constrainedSize];
XCTAssertTrue(CGSizeEqualToSizeWithIn(calculatedSize, recalculatedSize, 4.0), @"Recalculated size %@ should be same as original size %@", NSStringFromCGSize(recalculatedSize), NSStringFromCGSize(calculatedSize)); XCTAssertTrue(CGSizeEqualToSizeWithIn(calculatedSize, recalculatedSize, 4.0), @"Recalculated size %@ should be same as original size %@", NSStringFromCGSize(recalculatedSize), NSStringFromCGSize(calculatedSize));
} }
@@ -136,7 +136,7 @@ static BOOL CGSizeEqualToSizeWithIn(CGSize size1, CGSize size2, CGFloat delta)
for (CGFloat i = 10; i < 500; i *= 1.3) { for (CGFloat i = 10; i < 500; i *= 1.3) {
CGSize constrainedSize = CGSizeMake(i, i); CGSize constrainedSize = CGSizeMake(i, i);
CGSize calculatedSize = [_textNode measure:constrainedSize]; CGSize calculatedSize = [_textNode measure:constrainedSize];
CGSize recalculatedSize = [_textNode measure:calculatedSize]; CGSize recalculatedSize = [_textNode measure:constrainedSize];
XCTAssertTrue(CGSizeEqualToSizeWithIn(calculatedSize, recalculatedSize, 11.0), @"Recalculated size %@ should be same as original size %@", NSStringFromCGSize(recalculatedSize), NSStringFromCGSize(calculatedSize)); XCTAssertTrue(CGSizeEqualToSizeWithIn(calculatedSize, recalculatedSize, 11.0), @"Recalculated size %@ should be same as original size %@", NSStringFromCGSize(recalculatedSize), NSStringFromCGSize(calculatedSize));
} }