Skip to content

Commit

Permalink
Fix main thread assertion for tint color on text nodes, re-render tin…
Browse files Browse the repository at this point in the history
…t-able nodes on hierarchy changes (#1670)

- Address various issues with tint color on text nodes
- Avoid accessing `self.tintColor` off-main since this will trigger an
assertion and might be undefined behavior
- Trigger setNeedsDisplay in cases of hierarchy change and
tintColorDidChange in ASButtonNode, ASTextNode{1,2}
- Fix bug in _ASTableViewCell where we were overwriting the tint color
instead of inheriting the correct value
- Add tests for new logic in changing hierarchy
  • Loading branch information
rahul-malik committed Sep 16, 2019
1 parent e5eba11 commit 618f21e
Show file tree
Hide file tree
Showing 15 changed files with 196 additions and 17 deletions.
3 changes: 3 additions & 0 deletions Source/ASButtonNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,12 @@ -(void)tintColorDidChange
// UIButton documentation states that it tints the image and title of buttons when tintColor is set.
// | "The tint color to apply to the button title and image."
// | From: https://developer.apple.com/documentation/uikit/uibutton/1624025-tintcolor
[self lock];
UIColor *tintColor = self.tintColor;
self.imageNode.tintColor = tintColor;
self.titleNode.tintColor = tintColor;
[self unlock];
[self setNeedsDisplay];
}

- (void)updateImage
Expand Down
6 changes: 5 additions & 1 deletion Source/ASTableView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#import <AsyncDisplayKit/ASDelegateProxy.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
#import <AsyncDisplayKit/ASElementMap.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASLayout.h>
Expand All @@ -28,6 +29,7 @@
#import <AsyncDisplayKit/ASTableLayoutController.h>
#import <AsyncDisplayKit/ASBatchContext.h>


static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";

//#define LOG(...) NSLog(__VA_ARGS__)
Expand Down Expand Up @@ -113,12 +115,14 @@ - (void)setElement:(ASCollectionElement *)element
self.selectionStyle = node.selectionStyle;
self.focusStyle = node.focusStyle;
self.accessoryType = node.accessoryType;
self.tintColor = node.tintColor;

// the following ensures that we clip the entire cell to it's bounds if node.clipsToBounds is set (the default)
// This is actually a workaround for a bug we are seeing in some rare cases (selected background view
// overlaps other cells if size of ASCellNode has changed.)
self.clipsToBounds = node.clipsToBounds;

// If the cell node has been explicitly configured with a tint color, we can apply that directly to the cell view to preserve the previous behavior
self.tintColor = node->_tintColor;
}

[node __setSelectedFromUIKit:self.selected];
Expand Down
42 changes: 40 additions & 2 deletions Source/ASTextNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ @implementation ASTextNode {
CGSize _shadowOffset;
CGColorRef _shadowColor;
UIColor *_cachedShadowUIColor;
UIColor *_cachedTintColor;
UIColor *_placeholderColor;
CGFloat _shadowOpacity;
CGFloat _shadowRadius;
Expand Down Expand Up @@ -382,7 +383,7 @@ - (ASTextKitAttributes)_locked_rendererAttributes
.shadowColor = _cachedShadowUIColor,
.shadowOpacity = _shadowOpacity,
.shadowRadius = _shadowRadius,
.tintColor = self.textColorFollowsTintColor ? self.tintColor : nil
.tintColor = _cachedTintColor
};
}

Expand Down Expand Up @@ -543,7 +544,11 @@ - (NSArray *)exclusionPaths
- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer
{
ASLockScopeSelf();

if (_textColorFollowsTintColor) {
_cachedTintColor = self.tintColor;
} else {
_cachedTintColor = nil;
}
return [[ASTextNodeDrawParameter alloc] initWithRendererAttributes:[self _locked_rendererAttributes]
backgroundColor:self.backgroundColor
textContainerInsets:_textContainerInset
Expand Down Expand Up @@ -945,6 +950,39 @@ - (CGRect)frameForTextRange:(NSRange)textRange
return ASTextNodeAdjustRenderRectForShadowPadding(frame, self.shadowPadding);
}

#pragma mark - Tint Colors


- (void)tintColorDidChange
{
[super tintColorDidChange];

[self _setNeedsDisplayOnTintedTextColor];
}

- (void)_setNeedsDisplayOnTintedTextColor
{
BOOL textColorFollowsTintColor = NO;
{
AS::MutexLocker l(__instanceLock__);
textColorFollowsTintColor = _textColorFollowsTintColor;
}

if (textColorFollowsTintColor) {
[self setNeedsDisplay];
}
}


#pragma mark Interface State

- (void)didEnterHierarchy
{
[super didEnterHierarchy];

[self _setNeedsDisplayOnTintedTextColor];
}

#pragma mark - Placeholders

- (UIColor *)placeholderColor
Expand Down
60 changes: 46 additions & 14 deletions Source/ASTextNode2.mm
Original file line number Diff line number Diff line change
Expand Up @@ -511,19 +511,6 @@ - (void)prepareAttributedString:(NSMutableAttributedString *)attributedString is
shadow.shadowBlurRadius = _shadowRadius;
[attributedString addAttribute:NSShadowAttributeName value:shadow range:NSMakeRange(0, attributedString.length)];
}

// Apply tint color if needed and foreground color is not already specified
if (self.textColorFollowsTintColor && attributedString.length > 0) {
// Apply tint color if specified and if foreground color is undefined for attributedString
NSRange limit = NSMakeRange(0, attributedString.length);
// Look for previous attributes that define foreground color
UIColor *attributeValue = (UIColor *)[attributedString attribute:NSForegroundColorAttributeName atIndex:limit.location effectiveRange:NULL];
UIColor *tintColor = self.tintColor;
if (attributeValue == nil && tintColor) {
// None are found, apply tint color if available. Fallback to "black" text color
[attributedString addAttributes:@{ NSForegroundColorAttributeName : tintColor } range:limit];
}
}
}

#pragma mark - Drawing
Expand All @@ -540,7 +527,20 @@ - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer
NSMutableAttributedString *mutableText = [_attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init];

[self prepareAttributedString:mutableText isForIntrinsicSize:NO];


// After all other attributes are set, apply tint color if needed and foreground color is not already specified
if (self.textColorFollowsTintColor && mutableText.length > 0) {
// Apply tint color if specified and if foreground color is undefined for attributedString
NSRange limit = NSMakeRange(0, mutableText.length);
// Look for previous attributes that define foreground color
UIColor *attributeValue = (UIColor *)[mutableText attribute:NSForegroundColorAttributeName atIndex:limit.location effectiveRange:NULL];
UIColor *tintColor = self.tintColor;
if (attributeValue == nil && tintColor) {
// None are found, apply tint color if available. Fallback to "black" text color
[mutableText addAttributes:@{ NSForegroundColorAttributeName : tintColor } range:limit];
}
}

return @{
@"container": copiedContainer,
@"text": mutableText,
Expand Down Expand Up @@ -577,6 +577,38 @@ + (void)drawRect:(CGRect)bounds withParameters:(NSDictionary *)layoutDict isCanc
[layout drawInContext:context size:bounds.size point:bounds.origin view:nil layer:nil debug:[ASTextDebugOption sharedDebugOption] cancel:isCancelledBlock];
}

#pragma mark - Tint Color

- (void)tintColorDidChange
{
[super tintColorDidChange];

[self _setNeedsDisplayOnTintedTextColor];
}

- (void)_setNeedsDisplayOnTintedTextColor
{
BOOL textColorFollowsTintColor = NO;
{
AS::MutexLocker l(__instanceLock__);
textColorFollowsTintColor = _textColorFollowsTintColor;
}

if (textColorFollowsTintColor) {
[self setNeedsDisplay];
}
}


#pragma mark Interface State

- (void)didEnterHierarchy
{
[super didEnterHierarchy];

[self _setNeedsDisplayOnTintedTextColor];
}

#pragma mark - Attributes

- (id)linkAttributeValueAtPoint:(CGPoint)point
Expand Down
1 change: 1 addition & 0 deletions Source/Private/ASDisplayNode+UIViewBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,7 @@ - (void)setTintColor:(UIColor *)color
}
}
} else {
_tintColor = color;
_setToViewOnly(tintColor, color);
}
__instanceLock__.unlock();
Expand Down
32 changes: 32 additions & 0 deletions Tests/ASButtonNodeSnapshotTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,36 @@ - (void)testTintColorWithForegroundColorSet
ASSnapshotVerifyNode(node, nil);
}

- (void)testTintColorWithInheritedTintColor
{
ASDisplayNode *container = [[ASDisplayNode alloc] init];
container.tintColor = UIColor.redColor;

// Add to hierarchy, assert new tint is picked up
ASButtonNode *node = [[ASButtonNode alloc] init];
[container addSubnode:node];
[node setImage:[[self testImage] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
[node setTitle:@"Press Me"
withFont:[UIFont systemFontOfSize:48]
withColor:nil
forState:UIControlStateNormal];
node.imageNode.style.width = ASDimensionMake(200);
node.imageNode.style.height = ASDimensionMake(200);
container.style.preferredSize = CGSizeMake(1000,1000);
ASDisplayNodeSizeToFitSize(node, CGSizeMake(1000, 1000));
// Force load so the superview is set for the button _ASDisplayView
__unused UIView *v = container.view;
ASSnapshotVerifyNode(node, @"red_inherited_tint");

// Change hierarchy, assert new tint is picked up
ASDisplayNode *container2 = [[ASDisplayNode alloc] init];
container2.tintColor = UIColor.greenColor;
container2.style.preferredSize = CGSizeMake(1000,1000);
[container2 addSubnode:node];
ASDisplayNodeSizeToFitSize(node, CGSizeMake(1000, 1000));
// Force load so the superview is set for the button _ASDisplayView
__unused UIView *v2 = container2.view; // Force load
ASSnapshotVerifyNode(node, @"green_inherited_tint");
}

@end
21 changes: 21 additions & 0 deletions Tests/ASTableViewTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ - (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath
ASTestTextCellNode *textCellNode = [ASTestTextCellNode new];
textCellNode.text = [NSString stringWithFormat:@"{%d, %d}", (int)indexPath.section, (int)indexPath.row];
textCellNode.backgroundColor = [UIColor whiteColor];
textCellNode.tintColor = [UIColor yellowColor];
return textCellNode;
};
}
Expand Down Expand Up @@ -886,6 +887,26 @@ - (void)testTableViewReloadDoesReloadIfEditableTextNodeIsFirstResponder
XCTAssertEqual([node.view numberOfRowsInSection:0], 2);
}


- (void)testTintColorIsPropagatedToTableViewCell
{
// If a tint color is explicitly defined on an ASCellNode, we should
CGSize tableViewSize = CGSizeMake(100, 500);
ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height)
style:UITableViewStylePlain];
ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new];

tableView.asyncDelegate = dataSource;
tableView.asyncDataSource = dataSource;

[tableView reloadData];
[tableView waitUntilAllUpdatesAreCommitted];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
UITableViewCell *uikitCell = [tableView cellForRowAtIndexPath:indexPath];
BOOL areColorsEqual = CGColorEqualToColor(uikitCell.tintColor.CGColor, UIColor.yellowColor.CGColor);
XCTAssertTrue(areColorsEqual);
}

@end

@implementation UITableView (Testing)
Expand Down
23 changes: 23 additions & 0 deletions Tests/ASTextNode2SnapshotTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,27 @@ - (void)testTextTintColor_ASTextNode2
ASSnapshotVerifyNode(textNode, nil);
}


- (void)testTintColorHierarchyChange
{
ASDisplayNode *containerNode = [[ASDisplayNode alloc] init];
containerNode.tintColor = [UIColor greenColor];

ASTextNode *node = [[ASTextNode alloc] init];
[containerNode addSubnode:node];
[node setLayerBacked:YES];
node.textColorFollowsTintColor = YES;
node.attributedText = [[NSAttributedString alloc] initWithString:@"Hello" attributes:@{}];
ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY)));
containerNode.style.preferredSize = node.bounds.size;
ASSnapshotVerifyNode(node, @"green_tint_from_parent");

ASDisplayNode *containerNode2 = [[ASDisplayNode alloc] init];
containerNode2.tintColor = [UIColor redColor];
[containerNode2 addSubnode:node];
containerNode2.style.preferredSize = node.bounds.size;
ASSnapshotVerifyNode(node, @"red_tint_from_parent");
}


@end
25 changes: 25 additions & 0 deletions Tests/ASTextNodeSnapshotTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,28 @@ - (void)testUIGraphicsRendererDrawingExperiment
ASSnapshotVerifyNode(textNode, nil);
}

- (void)testTintColorHierarchyChange
{
ASDisplayNode *containerNode = [[ASDisplayNode alloc] init];
containerNode.tintColor = [UIColor greenColor];

ASTextNode *node = [[ASTextNode alloc] init];
[containerNode addSubnode:node];
[node setLayerBacked:YES];
node.textColorFollowsTintColor = YES;
node.attributedText = [[NSAttributedString alloc] initWithString:@"Hello" attributes:@{}];
ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY)));
containerNode.style.preferredSize = node.bounds.size;
ASSnapshotVerifyNode(node, @"green_tint_from_parent");


ASDisplayNode *containerNode2 = [[ASDisplayNode alloc] init];
containerNode2.tintColor = [UIColor redColor];
[containerNode2 addSubnode:node];
containerNode2.style.preferredSize = node.bounds.size;
ASSnapshotVerifyNode(node, @"red_tint_from_parent");
}

#if AS_AT_LEAST_IOS13

- (void)testUserInterfaceStyleSnapshotTesting
Expand Down Expand Up @@ -248,6 +270,9 @@ - (void)testUserInterfaceStyleSnapshotTestingTintColor
}
}




#endif // #if AS_AT_LEAST_IOS13

@end
Binary file added ...deSnapshotTests/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ...NodeSnapshotTests/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ...xtNode2SnapshotTests/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ...TextNode2SnapshotTests/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ...extNodeSnapshotTests/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ...STextNodeSnapshotTests/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 618f21e

Please sign in to comment.